{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "62b3e60b",
   "metadata": {},
   "outputs": [],
   "source": [
    "import warnings\n",
    "warnings.filterwarnings('ignore')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "f1b2cf07",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import gc\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import re\n",
    "import time\n",
    "from scipy import stats\n",
    "import matplotlib.pyplot as plt\n",
    "import category_encoders as ce\n",
    "import networkx as nx\n",
    "import pickle\n",
    "import lightgbm as lgb\n",
    "import catboost as cat\n",
    "import xgboost as xgb\n",
    "from datetime import timedelta\n",
    "from gensim.models import Word2Vec\n",
    "from io import StringIO\n",
    "from tqdm import tqdm\n",
    "from lightgbm import LGBMClassifier\n",
    "from lightgbm import log_evaluation, early_stopping\n",
    "from sklearn.metrics import roc_curve\n",
    "from scipy.stats import chi2_contingency, pearsonr\n",
    "from sklearn.preprocessing import StandardScaler, OneHotEncoder, LabelEncoder\n",
    "from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer\n",
    "from sklearn.feature_extraction import FeatureHasher\n",
    "from sklearn.model_selection import StratifiedKFold, KFold, train_test_split, GridSearchCV\n",
    "from category_encoders import TargetEncoder\n",
    "from sklearn.decomposition import TruncatedSVD\n",
    "from autogluon.tabular import TabularDataset, TabularPredictor, FeatureMetadata\n",
    "from autogluon.features.generators import AsTypeFeatureGenerator, BulkFeatureGenerator, DropUniqueFeatureGenerator, FillNaFeatureGenerator, PipelineFeatureGenerator\n",
    "from autogluon.features.generators import CategoryFeatureGenerator, IdentityFeatureGenerator, AutoMLPipelineFeatureGenerator\n",
    "from autogluon.common.features.types import R_INT, R_FLOAT\n",
    "from autogluon.core.metrics import make_scorer"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "56d1c0f9",
   "metadata": {},
   "source": [
    "# 数据导入"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7529f127",
   "metadata": {},
   "source": [
    "## 通用导入函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "0b7021c6",
   "metadata": {},
   "outputs": [],
   "source": [
    "def load_data_from_directory(directory):\n",
    "    \"\"\"\n",
    "    遍历目录加载所有CSV文件，将其作为独立的DataFrame变量\n",
    "\n",
    "    参数:\n",
    "    - directory: 输入的数据路径\n",
    "    \n",
    "    返回:\n",
    "    - 含有数据集名称的列表\n",
    "    \"\"\"\n",
    "    dataset_names = []\n",
    "    for filename in os.listdir(directory):\n",
    "        if filename.endswith(\".csv\"):\n",
    "            dataset_name = os.path.splitext(filename)[0] + '_data' # 获取文件名作为变量名\n",
    "            file_path = os.path.join(directory, filename)  # 完整的文件路径\n",
    "            globals()[dataset_name] = pd.read_csv(file_path)  # 将文件加载为DataFrame并赋值给全局变量\n",
    "            dataset_names.append(dataset_name)\n",
    "            print(f\"数据集 {dataset_name} 已加载为 DataFrame\")\n",
    "\n",
    "    return dataset_names"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8434568a",
   "metadata": {},
   "source": [
    "## 训练集导入"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "c410a626",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "数据集 AGET_PAY_data 已加载为 DataFrame\n",
      "数据集 ASSET_data 已加载为 DataFrame\n",
      "数据集 CCD_TR_DTL_data 已加载为 DataFrame\n",
      "数据集 MB_PAGEVIEW_DTL_data 已加载为 DataFrame\n",
      "数据集 MB_QRYTRNFLW_data 已加载为 DataFrame\n",
      "数据集 MB_TRNFLW_data 已加载为 DataFrame\n",
      "数据集 NATURE_data 已加载为 DataFrame\n",
      "数据集 PROD_HOLD_data 已加载为 DataFrame\n",
      "数据集 TARGET_data 已加载为 DataFrame\n",
      "数据集 TARGET_VALID_data 已加载为 DataFrame\n",
      "数据集 MB_PAGEVIEW_DTL_data 已加载为 DataFrame\n",
      "数据集 MB_QRYTRNFLW_data 已加载为 DataFrame\n",
      "数据集 MB_TRNFLW_data 已加载为 DataFrame\n",
      "数据集 NATURE_data 已加载为 DataFrame\n",
      "数据集 PROD_HOLD_data 已加载为 DataFrame\n",
      "数据集 TARGET_data 已加载为 DataFrame\n",
      "数据集 TARGET_VALID_data 已加载为 DataFrame\n",
      "数据集 TR_APS_DTL_data 已加载为 DataFrame\n",
      "数据集 TR_IBTF_data 已加载为 DataFrame\n",
      "数据集 TR_TPAY_data 已加载为 DataFrame\n",
      "数据集 TR_APS_DTL_data 已加载为 DataFrame\n",
      "数据集 TR_IBTF_data 已加载为 DataFrame\n",
      "数据集 TR_TPAY_data 已加载为 DataFrame\n"
     ]
    }
   ],
   "source": [
    "train_load_dt = '../DATA'\n",
    "train_data_name = load_data_from_directory(train_load_dt)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a8812a6c",
   "metadata": {},
   "source": [
    "# 特征工程"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "2a78da18",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✓ 特征工程函数定义完成\n"
     ]
    }
   ],
   "source": [
    "# 特征工程函数定义\n",
    "import warnings\n",
    "warnings.filterwarnings('ignore')\n",
    "\n",
    "def get_id_category_features(df_fea, df_to_groupby, fea1, fea2, stat):\n",
    "    \"\"\"\n",
    "    对分类特征进行分组统计\n",
    "    参数:\n",
    "        df_fea: 特征数据框\n",
    "        df_to_groupby: 待分组的数据框\n",
    "        fea1: 分组特征1(如模块名称)\n",
    "        fea2: 统计特征(如点击次数)\n",
    "        stat: 统计方法\n",
    "    \"\"\"\n",
    "    tmp = df_to_groupby.groupby(['CUST_NO', fea1])[fea2].agg(\n",
    "        stat if stat != \"kurt\" else lambda x: x.kurt()\n",
    "    ).to_frame(\n",
    "        '_'.join(['CUST_NO', fea1, fea2, stat])\n",
    "    ).reset_index()\n",
    "    \n",
    "    df_tmp = pd.pivot(data=tmp, index='CUST_NO', columns=fea1, values='_'.join(['CUST_NO', fea1, fea2, stat]))\n",
    "    new_fea_cols = ['_'.join(['CUST_NO', fea1, fea2, stat, str(col)]) for col in df_tmp.columns]\n",
    "    df_tmp.columns = new_fea_cols\n",
    "    df_tmp.reset_index(inplace=True)\n",
    "        \n",
    "    if stat == 'count':\n",
    "        df_tmp = df_tmp.fillna(0)\n",
    "        \n",
    "    # 去掉全NaN列\n",
    "    valid_cols = []\n",
    "    for col in df_tmp.columns:\n",
    "        if not df_tmp[col].isna().all():\n",
    "            valid_cols.append(col)\n",
    "            \n",
    "    df_fea = df_fea.merge(df_tmp[valid_cols], on='CUST_NO', how='left')\n",
    "    return df_fea, new_fea_cols \n",
    "\n",
    "def get_all_id_category_features(df_fea, df_to_groupby, fea1, fea2, stats):\n",
    "    \"\"\"批量对分类特征进行多种统计\"\"\"\n",
    "    all_new_fea_cols = []\n",
    "    for stat in tqdm(stats, desc=f'Processing {fea1}-{fea2}'):\n",
    "        df_fea, new_fea_cols = get_id_category_features(df_fea, df_to_groupby, fea1, fea2, stat)\n",
    "        all_new_fea_cols += new_fea_cols\n",
    "    return df_fea, all_new_fea_cols\n",
    "\n",
    "print(\"✓ 特征工程函数定义完成\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3e563753",
   "metadata": {},
   "source": [
    "## 第一部分: 基础时间特征 (Time-based Features)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "1f0bb1ce",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "数据形状: (372196, 14)\n",
      "时间范围: 2025-04-01 00:00:00 ~ 2025-06-30 00:00:00\n",
      "月份分布:\n",
      "date_months_to_now\n",
      "0    132174\n",
      "1    122815\n",
      "2    117207\n",
      "Name: count, dtype: int64\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>OPERATION_DATE</th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>PAGE_TITLE</th>\n",
       "      <th>REFERRER_TITLE</th>\n",
       "      <th>MODEL_NAME</th>\n",
       "      <th>date_months_to_now</th>\n",
       "      <th>date_weeks_to_now</th>\n",
       "      <th>date_days_to_now</th>\n",
       "      <th>operation_month</th>\n",
       "      <th>operation_day</th>\n",
       "      <th>operation_dayofweek</th>\n",
       "      <th>operation_is_weekend</th>\n",
       "      <th>operation_is_month_start</th>\n",
       "      <th>operation_is_month_end</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>2025-04-07</td>\n",
       "      <td>864a14a62ffffbc4741d365ea5a08278</td>\n",
       "      <td>dc127d306179477fef4f3a9378dc550b</td>\n",
       "      <td>0e5c9561153e8b3fd936b94a5641c8e1</td>\n",
       "      <td>c5b386b7a6348a2f1ba70f2259fb827e</td>\n",
       "      <td>2</td>\n",
       "      <td>12</td>\n",
       "      <td>84</td>\n",
       "      <td>4</td>\n",
       "      <td>7</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2025-04-07</td>\n",
       "      <td>864a14a62ffffbc4741d365ea5a08278</td>\n",
       "      <td>0e5c9561153e8b3fd936b94a5641c8e1</td>\n",
       "      <td>c5b386b7a6348a2f1ba70f2259fb827e</td>\n",
       "      <td>c5b386b7a6348a2f1ba70f2259fb827e</td>\n",
       "      <td>2</td>\n",
       "      <td>12</td>\n",
       "      <td>84</td>\n",
       "      <td>4</td>\n",
       "      <td>7</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2025-04-07</td>\n",
       "      <td>864a14a62ffffbc4741d365ea5a08278</td>\n",
       "      <td>a3efea933884689e89b46cadd9aa989e</td>\n",
       "      <td>dc127d306179477fef4f3a9378dc550b</td>\n",
       "      <td>c5b386b7a6348a2f1ba70f2259fb827e</td>\n",
       "      <td>2</td>\n",
       "      <td>12</td>\n",
       "      <td>84</td>\n",
       "      <td>4</td>\n",
       "      <td>7</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>2025-04-07</td>\n",
       "      <td>74dfe9a67327540d1f427b40e85d49c7</td>\n",
       "      <td>dc127d306179477fef4f3a9378dc550b</td>\n",
       "      <td>a3efea933884689e89b46cadd9aa989e</td>\n",
       "      <td>c5b386b7a6348a2f1ba70f2259fb827e</td>\n",
       "      <td>2</td>\n",
       "      <td>12</td>\n",
       "      <td>84</td>\n",
       "      <td>4</td>\n",
       "      <td>7</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>2025-04-07</td>\n",
       "      <td>74dfe9a67327540d1f427b40e85d49c7</td>\n",
       "      <td>dc127d306179477fef4f3a9378dc550b</td>\n",
       "      <td>c5b386b7a6348a2f1ba70f2259fb827e</td>\n",
       "      <td>c5b386b7a6348a2f1ba70f2259fb827e</td>\n",
       "      <td>2</td>\n",
       "      <td>12</td>\n",
       "      <td>84</td>\n",
       "      <td>4</td>\n",
       "      <td>7</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "  OPERATION_DATE                           CUST_NO  \\\n",
       "0     2025-04-07  864a14a62ffffbc4741d365ea5a08278   \n",
       "1     2025-04-07  864a14a62ffffbc4741d365ea5a08278   \n",
       "2     2025-04-07  864a14a62ffffbc4741d365ea5a08278   \n",
       "3     2025-04-07  74dfe9a67327540d1f427b40e85d49c7   \n",
       "4     2025-04-07  74dfe9a67327540d1f427b40e85d49c7   \n",
       "\n",
       "                         PAGE_TITLE                    REFERRER_TITLE  \\\n",
       "0  dc127d306179477fef4f3a9378dc550b  0e5c9561153e8b3fd936b94a5641c8e1   \n",
       "1  0e5c9561153e8b3fd936b94a5641c8e1  c5b386b7a6348a2f1ba70f2259fb827e   \n",
       "2  a3efea933884689e89b46cadd9aa989e  dc127d306179477fef4f3a9378dc550b   \n",
       "3  dc127d306179477fef4f3a9378dc550b  a3efea933884689e89b46cadd9aa989e   \n",
       "4  dc127d306179477fef4f3a9378dc550b  c5b386b7a6348a2f1ba70f2259fb827e   \n",
       "\n",
       "                         MODEL_NAME  date_months_to_now  date_weeks_to_now  \\\n",
       "0  c5b386b7a6348a2f1ba70f2259fb827e                   2                 12   \n",
       "1  c5b386b7a6348a2f1ba70f2259fb827e                   2                 12   \n",
       "2  c5b386b7a6348a2f1ba70f2259fb827e                   2                 12   \n",
       "3  c5b386b7a6348a2f1ba70f2259fb827e                   2                 12   \n",
       "4  c5b386b7a6348a2f1ba70f2259fb827e                   2                 12   \n",
       "\n",
       "   date_days_to_now  operation_month  operation_day  operation_dayofweek  \\\n",
       "0                84                4              7                    0   \n",
       "1                84                4              7                    0   \n",
       "2                84                4              7                    0   \n",
       "3                84                4              7                    0   \n",
       "4                84                4              7                    0   \n",
       "\n",
       "   operation_is_weekend  operation_is_month_start  operation_is_month_end  \n",
       "0                     0                         0                       0  \n",
       "1                     0                         0                       0  \n",
       "2                     0                         0                       0  \n",
       "3                     0                         0                       0  \n",
       "4                     0                         0                       0  "
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 1. 时间特征工程\n",
    "def get_days_to_now(df):\n",
    "    \"\"\"将操作日期转换为距今天数\"\"\"\n",
    "    df = df.copy()\n",
    "    # 自动检测日期格式\n",
    "    if df[\"OPERATION_DATE\"].dtype == 'object':\n",
    "        # 尝试不同的日期格式\n",
    "        try:\n",
    "            df[\"OPERATION_DATE\"] = pd.to_datetime(df[\"OPERATION_DATE\"], format=\"%Y%m%d\")\n",
    "        except:\n",
    "            df[\"OPERATION_DATE\"] = pd.to_datetime(df[\"OPERATION_DATE\"])\n",
    "    \n",
    "    df_days_to_now = (df[\"OPERATION_DATE\"].max() - df[\"OPERATION_DATE\"]).dt.days\n",
    "    df[\"date_months_to_now\"] = df_days_to_now // 31  # 距今月数\n",
    "    df[\"date_weeks_to_now\"] = df_days_to_now // 7   # 距今周数\n",
    "    df[\"date_days_to_now\"] = df_days_to_now          # 距今天数\n",
    "    \n",
    "    # 提取时间维度特征\n",
    "    df[\"operation_month\"] = df[\"OPERATION_DATE\"].dt.month\n",
    "    df[\"operation_day\"] = df[\"OPERATION_DATE\"].dt.day\n",
    "    df[\"operation_dayofweek\"] = df[\"OPERATION_DATE\"].dt.dayofweek  # 周几\n",
    "    df[\"operation_is_weekend\"] = df[\"operation_dayofweek\"].isin([5, 6]).astype(int)\n",
    "    df[\"operation_is_month_start\"] = df[\"OPERATION_DATE\"].dt.is_month_start.astype(int)\n",
    "    df[\"operation_is_month_end\"] = df[\"OPERATION_DATE\"].dt.is_month_end.astype(int)\n",
    "    \n",
    "    return df\n",
    "\n",
    "# 处理数据\n",
    "MB_PAGEVIEW_DTL_data = get_days_to_now(MB_PAGEVIEW_DTL_data)\n",
    "print(f\"数据形状: {MB_PAGEVIEW_DTL_data.shape}\")\n",
    "print(f\"时间范围: {MB_PAGEVIEW_DTL_data['OPERATION_DATE'].min()} ~ {MB_PAGEVIEW_DTL_data['OPERATION_DATE'].max()}\")\n",
    "print(f\"月份分布:\\n{MB_PAGEVIEW_DTL_data['date_months_to_now'].value_counts().sort_index()}\")\n",
    "MB_PAGEVIEW_DTL_data.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "79a86275",
   "metadata": {},
   "source": [
    "## 第二部分: RFM特征 (Recency, Frequency, Monetary)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "9701c1d2",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "RFM-R特征: 100%|██████████| 3/3 [00:00<00:00, 83.38it/s]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✓ 基础RFM特征数量: 18\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>max_nunique_days_to_now_0</th>\n",
       "      <th>max_count_days_to_now_0</th>\n",
       "      <th>max_nunique_days_to_now_1</th>\n",
       "      <th>max_count_days_to_now_1</th>\n",
       "      <th>max_nunique_days_to_now_2</th>\n",
       "      <th>max_count_days_to_now_2</th>\n",
       "      <th>daily_page_nunique_mean</th>\n",
       "      <th>daily_page_count_mean</th>\n",
       "      <th>daily_page_nunique_max</th>\n",
       "      <th>daily_page_count_max</th>\n",
       "      <th>daily_page_nunique_min</th>\n",
       "      <th>daily_page_count_min</th>\n",
       "      <th>daily_page_nunique_median</th>\n",
       "      <th>daily_page_count_median</th>\n",
       "      <th>daily_page_nunique_std</th>\n",
       "      <th>daily_page_count_std</th>\n",
       "      <th>daily_page_nunique_sum</th>\n",
       "      <th>daily_page_count_sum</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>864a14a62ffffbc4741d365ea5a08278</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>76.0</td>\n",
       "      <td>76.0</td>\n",
       "      <td>3.250000</td>\n",
       "      <td>4.666667</td>\n",
       "      <td>6</td>\n",
       "      <td>12</td>\n",
       "      <td>3</td>\n",
       "      <td>3</td>\n",
       "      <td>3.0</td>\n",
       "      <td>3.5</td>\n",
       "      <td>0.866025</td>\n",
       "      <td>2.774341</td>\n",
       "      <td>39</td>\n",
       "      <td>56</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>74dfe9a67327540d1f427b40e85d49c7</td>\n",
       "      <td>5.0</td>\n",
       "      <td>26.0</td>\n",
       "      <td>51.0</td>\n",
       "      <td>56.0</td>\n",
       "      <td>67.0</td>\n",
       "      <td>67.0</td>\n",
       "      <td>3.739130</td>\n",
       "      <td>5.782609</td>\n",
       "      <td>7</td>\n",
       "      <td>14</td>\n",
       "      <td>3</td>\n",
       "      <td>3</td>\n",
       "      <td>3.0</td>\n",
       "      <td>5.0</td>\n",
       "      <td>1.053884</td>\n",
       "      <td>3.044466</td>\n",
       "      <td>86</td>\n",
       "      <td>133</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>078f92ffdfdfe07bb958c10c01d0733c</td>\n",
       "      <td>15.0</td>\n",
       "      <td>15.0</td>\n",
       "      <td>60.0</td>\n",
       "      <td>42.0</td>\n",
       "      <td>73.0</td>\n",
       "      <td>84.0</td>\n",
       "      <td>4.500000</td>\n",
       "      <td>7.500000</td>\n",
       "      <td>9</td>\n",
       "      <td>19</td>\n",
       "      <td>2</td>\n",
       "      <td>3</td>\n",
       "      <td>4.0</td>\n",
       "      <td>6.5</td>\n",
       "      <td>1.303840</td>\n",
       "      <td>3.569314</td>\n",
       "      <td>117</td>\n",
       "      <td>195</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>c1d381f50813111b4f1f8d3b124c369f</td>\n",
       "      <td>4.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>45.0</td>\n",
       "      <td>42.0</td>\n",
       "      <td>68.0</td>\n",
       "      <td>68.0</td>\n",
       "      <td>3.904762</td>\n",
       "      <td>6.619048</td>\n",
       "      <td>7</td>\n",
       "      <td>24</td>\n",
       "      <td>3</td>\n",
       "      <td>3</td>\n",
       "      <td>4.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>1.179185</td>\n",
       "      <td>5.103687</td>\n",
       "      <td>82</td>\n",
       "      <td>139</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>3f60ef34ad3853819e4269469280177d</td>\n",
       "      <td>27.0</td>\n",
       "      <td>26.0</td>\n",
       "      <td>38.0</td>\n",
       "      <td>47.0</td>\n",
       "      <td>81.0</td>\n",
       "      <td>75.0</td>\n",
       "      <td>6.125000</td>\n",
       "      <td>10.343750</td>\n",
       "      <td>16</td>\n",
       "      <td>33</td>\n",
       "      <td>3</td>\n",
       "      <td>4</td>\n",
       "      <td>4.5</td>\n",
       "      <td>6.0</td>\n",
       "      <td>3.480360</td>\n",
       "      <td>8.334187</td>\n",
       "      <td>196</td>\n",
       "      <td>331</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  max_nunique_days_to_now_0  \\\n",
       "0  864a14a62ffffbc4741d365ea5a08278                        NaN   \n",
       "1  74dfe9a67327540d1f427b40e85d49c7                        5.0   \n",
       "2  078f92ffdfdfe07bb958c10c01d0733c                       15.0   \n",
       "3  c1d381f50813111b4f1f8d3b124c369f                        4.0   \n",
       "4  3f60ef34ad3853819e4269469280177d                       27.0   \n",
       "\n",
       "   max_count_days_to_now_0  max_nunique_days_to_now_1  \\\n",
       "0                      NaN                        NaN   \n",
       "1                     26.0                       51.0   \n",
       "2                     15.0                       60.0   \n",
       "3                      4.0                       45.0   \n",
       "4                     26.0                       38.0   \n",
       "\n",
       "   max_count_days_to_now_1  max_nunique_days_to_now_2  \\\n",
       "0                      NaN                       76.0   \n",
       "1                     56.0                       67.0   \n",
       "2                     42.0                       73.0   \n",
       "3                     42.0                       68.0   \n",
       "4                     47.0                       81.0   \n",
       "\n",
       "   max_count_days_to_now_2  daily_page_nunique_mean  daily_page_count_mean  \\\n",
       "0                     76.0                 3.250000               4.666667   \n",
       "1                     67.0                 3.739130               5.782609   \n",
       "2                     84.0                 4.500000               7.500000   \n",
       "3                     68.0                 3.904762               6.619048   \n",
       "4                     75.0                 6.125000              10.343750   \n",
       "\n",
       "   daily_page_nunique_max  daily_page_count_max  daily_page_nunique_min  \\\n",
       "0                       6                    12                       3   \n",
       "1                       7                    14                       3   \n",
       "2                       9                    19                       2   \n",
       "3                       7                    24                       3   \n",
       "4                      16                    33                       3   \n",
       "\n",
       "   daily_page_count_min  daily_page_nunique_median  daily_page_count_median  \\\n",
       "0                     3                        3.0                      3.5   \n",
       "1                     3                        3.0                      5.0   \n",
       "2                     3                        4.0                      6.5   \n",
       "3                     3                        4.0                      4.0   \n",
       "4                     4                        4.5                      6.0   \n",
       "\n",
       "   daily_page_nunique_std  daily_page_count_std  daily_page_nunique_sum  \\\n",
       "0                0.866025              2.774341                      39   \n",
       "1                1.053884              3.044466                      86   \n",
       "2                1.303840              3.569314                     117   \n",
       "3                1.179185              5.103687                      82   \n",
       "4                3.480360              8.334187                     196   \n",
       "\n",
       "   daily_page_count_sum  \n",
       "0                    56  \n",
       "1                   133  \n",
       "2                   195  \n",
       "3                   139  \n",
       "4                   331  "
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 2. RFM特征: 按天聚合统计\n",
    "def gen_mb_basic_features(df):\n",
    "    \"\"\"\n",
    "    生成掌银页面访问的基础RFM特征\n",
    "    - R (Recency): 最近访问时间\n",
    "    - F (Frequency): 访问频次\n",
    "    - M (Monetary): 访问页面种类\n",
    "    \"\"\"\n",
    "    feature = df[[\"CUST_NO\"]].drop_duplicates(['CUST_NO']).copy().reset_index(drop=True)\n",
    "    \n",
    "    # 按天聚合\n",
    "    df_by_day = df.groupby([\"CUST_NO\", \"date_days_to_now\", \"date_weeks_to_now\", \"date_months_to_now\"])[\"PAGE_TITLE\"].agg(['nunique', 'count'])\n",
    "    df_by_day.columns = ['PAGE_TITLE_nunique', 'PAGE_TITLE_count']\n",
    "    df_by_day = df_by_day.reset_index()\n",
    "    \n",
    "    # RFM-R: 每月日点击笔数/日点击页面数最大天距今天数\n",
    "    def get_max_cnt_days_to_now(df_month, month):\n",
    "        # 日点击页面数最多的那天距今天数\n",
    "        tmp_df_nunique = df_month.groupby(['CUST_NO']).agg({\"PAGE_TITLE_nunique\": \"max\"}).reset_index()\n",
    "        tmp_df_nunique = tmp_df_nunique.merge(\n",
    "            df_month[['CUST_NO', 'date_days_to_now', 'PAGE_TITLE_nunique']], \n",
    "            on=[\"CUST_NO\", 'PAGE_TITLE_nunique'], how=\"inner\"\n",
    "        )\n",
    "        tmp_df_nunique = tmp_df_nunique.groupby(['CUST_NO'])[\"date_days_to_now\"].min().to_frame(\n",
    "            f\"max_nunique_days_to_now_{month}\"\n",
    "        ).reset_index()\n",
    "        \n",
    "        # 日点击次数最多的那天距今天数\n",
    "        tmp_df_cnt = df_month.groupby(['CUST_NO']).agg({\"PAGE_TITLE_count\": \"max\"}).reset_index()\n",
    "        tmp_df_cnt = tmp_df_cnt.merge(\n",
    "            df_month[['CUST_NO', 'date_days_to_now', 'PAGE_TITLE_count']], \n",
    "            on=[\"CUST_NO\", 'PAGE_TITLE_count'], how=\"inner\"\n",
    "        )\n",
    "        tmp_df_cnt = tmp_df_cnt.groupby(['CUST_NO'])[\"date_days_to_now\"].min().to_frame(\n",
    "            f\"max_count_days_to_now_{month}\"\n",
    "        ).reset_index()\n",
    "        \n",
    "        return tmp_df_nunique, tmp_df_cnt\n",
    "    \n",
    "    # 分月统计\n",
    "    for month in tqdm([0, 1, 2], desc='RFM-R特征'):\n",
    "        data_month = df_by_day[df_by_day[\"date_months_to_now\"] == month]\n",
    "        tmp_df_nunique, tmp_df_cnt = get_max_cnt_days_to_now(data_month, month)\n",
    "        feature = feature.merge(tmp_df_nunique, how=\"left\", on=\"CUST_NO\")\n",
    "        feature = feature.merge(tmp_df_cnt, how=\"left\", on=\"CUST_NO\")\n",
    "    \n",
    "    # RFM-F: 整体访问频次统计\n",
    "    for stat in ['mean', 'max', 'min', 'median', 'std', 'sum']:\n",
    "        # 每日点击页面数统计\n",
    "        tmp = df_by_day.groupby(['CUST_NO'])['PAGE_TITLE_nunique'].agg(stat).to_frame(\n",
    "            f'daily_page_nunique_{stat}'\n",
    "        ).reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "        \n",
    "        # 每日点击次数统计\n",
    "        tmp = df_by_day.groupby(['CUST_NO'])['PAGE_TITLE_count'].agg(stat).to_frame(\n",
    "            f'daily_page_count_{stat}'\n",
    "        ).reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    return feature\n",
    "\n",
    "mb_basic_features = gen_mb_basic_features(MB_PAGEVIEW_DTL_data)\n",
    "print(f\"✓ 基础RFM特征数量: {mb_basic_features.shape[1] - 1}\")  # 减去CUST_NO\n",
    "mb_basic_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "132e6fdd",
   "metadata": {},
   "source": [
    "## 第三部分: 页面/模块访问路径特征 (Navigation Path Features)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "294760e1",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Top页面路径特征: 100%|██████████| 30/30 [00:00<00:00, 79.05it/s]\n",
      "Top模块路径特征: 100%|██████████| 30/30 [00:00<00:00, 70.91it/s]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✓ 导航路径特征数量: 66\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>unique_page_paths</th>\n",
       "      <th>total_page_visits</th>\n",
       "      <th>page_path_diversity</th>\n",
       "      <th>unique_model_paths</th>\n",
       "      <th>total_model_visits</th>\n",
       "      <th>model_path_diversity</th>\n",
       "      <th>page_path_count_c5b386b7a6348a2f1ba70f2259fb827e_dc127d306179477fef4f3a9378dc550b</th>\n",
       "      <th>page_path_count_c5b386b7a6348a2f1ba70f2259fb827e_0e5c9561153e8b3fd936b94a5641c8e1</th>\n",
       "      <th>page_path_count_c5b386b7a6348a2f1ba70f2259fb827e_cbbf501be82cabb897c45e7fcb13f167</th>\n",
       "      <th>...</th>\n",
       "      <th>model_path_count_bcf724833716039656a608713cad279f_bcf724833716039656a608713cad279f</th>\n",
       "      <th>model_path_count_75c9358b1c56ab43410e6a12ade1a393_75c9358b1c56ab43410e6a12ade1a393</th>\n",
       "      <th>model_path_count_878ac3a435a9e30b63c6b4c8c1806171_b989cf3952250c20ce3f5ce391638fbc</th>\n",
       "      <th>model_path_count_d70f9f4b8952b0a2ad8879c7eb3d8813_d70f9f4b8952b0a2ad8879c7eb3d8813</th>\n",
       "      <th>model_path_count_b7d64bf59eee872f66ab190a19478e5e_878ac3a435a9e30b63c6b4c8c1806171</th>\n",
       "      <th>model_path_count_81d3e1ea5b8de51bda4e9047a5773008_81d3e1ea5b8de51bda4e9047a5773008</th>\n",
       "      <th>model_path_count_efc38e763169ffe517f414056b005d00_efc38e763169ffe517f414056b005d00</th>\n",
       "      <th>model_path_count_b989cf3952250c20ce3f5ce391638fbc_878ac3a435a9e30b63c6b4c8c1806171</th>\n",
       "      <th>model_path_count_9fea9e924bdd0d9464ab9e90138556e7_c5b386b7a6348a2f1ba70f2259fb827e</th>\n",
       "      <th>model_path_count_c5b386b7a6348a2f1ba70f2259fb827e_56e6187b5b44989eea9322ccb9443036</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>864a14a62ffffbc4741d365ea5a08278</td>\n",
       "      <td>9</td>\n",
       "      <td>56</td>\n",
       "      <td>0.157895</td>\n",
       "      <td>6</td>\n",
       "      <td>56</td>\n",
       "      <td>0.105263</td>\n",
       "      <td>NaN</td>\n",
       "      <td>15.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>74dfe9a67327540d1f427b40e85d49c7</td>\n",
       "      <td>25</td>\n",
       "      <td>133</td>\n",
       "      <td>0.186567</td>\n",
       "      <td>13</td>\n",
       "      <td>133</td>\n",
       "      <td>0.097015</td>\n",
       "      <td>23.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>2.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>078f92ffdfdfe07bb958c10c01d0733c</td>\n",
       "      <td>32</td>\n",
       "      <td>195</td>\n",
       "      <td>0.163265</td>\n",
       "      <td>18</td>\n",
       "      <td>195</td>\n",
       "      <td>0.091837</td>\n",
       "      <td>10.0</td>\n",
       "      <td>16.0</td>\n",
       "      <td>2.0</td>\n",
       "      <td>...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>1.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>c1d381f50813111b4f1f8d3b124c369f</td>\n",
       "      <td>25</td>\n",
       "      <td>139</td>\n",
       "      <td>0.178571</td>\n",
       "      <td>15</td>\n",
       "      <td>139</td>\n",
       "      <td>0.107143</td>\n",
       "      <td>11.0</td>\n",
       "      <td>11.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>3.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>2.0</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>3f60ef34ad3853819e4269469280177d</td>\n",
       "      <td>69</td>\n",
       "      <td>331</td>\n",
       "      <td>0.207831</td>\n",
       "      <td>27</td>\n",
       "      <td>331</td>\n",
       "      <td>0.081325</td>\n",
       "      <td>27.0</td>\n",
       "      <td>12.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>...</td>\n",
       "      <td>2.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>5 rows × 67 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  unique_page_paths  total_page_visits  \\\n",
       "0  864a14a62ffffbc4741d365ea5a08278                  9                 56   \n",
       "1  74dfe9a67327540d1f427b40e85d49c7                 25                133   \n",
       "2  078f92ffdfdfe07bb958c10c01d0733c                 32                195   \n",
       "3  c1d381f50813111b4f1f8d3b124c369f                 25                139   \n",
       "4  3f60ef34ad3853819e4269469280177d                 69                331   \n",
       "\n",
       "   page_path_diversity  unique_model_paths  total_model_visits  \\\n",
       "0             0.157895                   6                  56   \n",
       "1             0.186567                  13                 133   \n",
       "2             0.163265                  18                 195   \n",
       "3             0.178571                  15                 139   \n",
       "4             0.207831                  27                 331   \n",
       "\n",
       "   model_path_diversity  \\\n",
       "0              0.105263   \n",
       "1              0.097015   \n",
       "2              0.091837   \n",
       "3              0.107143   \n",
       "4              0.081325   \n",
       "\n",
       "   page_path_count_c5b386b7a6348a2f1ba70f2259fb827e_dc127d306179477fef4f3a9378dc550b  \\\n",
       "0                                                NaN                                   \n",
       "1                                               23.0                                   \n",
       "2                                               10.0                                   \n",
       "3                                               11.0                                   \n",
       "4                                               27.0                                   \n",
       "\n",
       "   page_path_count_c5b386b7a6348a2f1ba70f2259fb827e_0e5c9561153e8b3fd936b94a5641c8e1  \\\n",
       "0                                               15.0                                   \n",
       "1                                                NaN                                   \n",
       "2                                               16.0                                   \n",
       "3                                               11.0                                   \n",
       "4                                               12.0                                   \n",
       "\n",
       "   page_path_count_c5b386b7a6348a2f1ba70f2259fb827e_cbbf501be82cabb897c45e7fcb13f167  \\\n",
       "0                                                NaN                                   \n",
       "1                                                NaN                                   \n",
       "2                                                2.0                                   \n",
       "3                                                NaN                                   \n",
       "4                                                1.0                                   \n",
       "\n",
       "   ...  \\\n",
       "0  ...   \n",
       "1  ...   \n",
       "2  ...   \n",
       "3  ...   \n",
       "4  ...   \n",
       "\n",
       "   model_path_count_bcf724833716039656a608713cad279f_bcf724833716039656a608713cad279f  \\\n",
       "0                                                NaN                                    \n",
       "1                                                NaN                                    \n",
       "2                                                NaN                                    \n",
       "3                                                NaN                                    \n",
       "4                                                2.0                                    \n",
       "\n",
       "   model_path_count_75c9358b1c56ab43410e6a12ade1a393_75c9358b1c56ab43410e6a12ade1a393  \\\n",
       "0                                                NaN                                    \n",
       "1                                                NaN                                    \n",
       "2                                                NaN                                    \n",
       "3                                                NaN                                    \n",
       "4                                                NaN                                    \n",
       "\n",
       "   model_path_count_878ac3a435a9e30b63c6b4c8c1806171_b989cf3952250c20ce3f5ce391638fbc  \\\n",
       "0                                                NaN                                    \n",
       "1                                                NaN                                    \n",
       "2                                                NaN                                    \n",
       "3                                                NaN                                    \n",
       "4                                                NaN                                    \n",
       "\n",
       "   model_path_count_d70f9f4b8952b0a2ad8879c7eb3d8813_d70f9f4b8952b0a2ad8879c7eb3d8813  \\\n",
       "0                                                NaN                                    \n",
       "1                                                NaN                                    \n",
       "2                                                NaN                                    \n",
       "3                                                3.0                                    \n",
       "4                                                NaN                                    \n",
       "\n",
       "   model_path_count_b7d64bf59eee872f66ab190a19478e5e_878ac3a435a9e30b63c6b4c8c1806171  \\\n",
       "0                                                NaN                                    \n",
       "1                                                NaN                                    \n",
       "2                                                NaN                                    \n",
       "3                                                NaN                                    \n",
       "4                                                NaN                                    \n",
       "\n",
       "   model_path_count_81d3e1ea5b8de51bda4e9047a5773008_81d3e1ea5b8de51bda4e9047a5773008  \\\n",
       "0                                                NaN                                    \n",
       "1                                                NaN                                    \n",
       "2                                                NaN                                    \n",
       "3                                                NaN                                    \n",
       "4                                                NaN                                    \n",
       "\n",
       "   model_path_count_efc38e763169ffe517f414056b005d00_efc38e763169ffe517f414056b005d00  \\\n",
       "0                                                NaN                                    \n",
       "1                                                NaN                                    \n",
       "2                                                NaN                                    \n",
       "3                                                NaN                                    \n",
       "4                                                NaN                                    \n",
       "\n",
       "   model_path_count_b989cf3952250c20ce3f5ce391638fbc_878ac3a435a9e30b63c6b4c8c1806171  \\\n",
       "0                                                NaN                                    \n",
       "1                                                2.0                                    \n",
       "2                                                1.0                                    \n",
       "3                                                NaN                                    \n",
       "4                                                NaN                                    \n",
       "\n",
       "   model_path_count_9fea9e924bdd0d9464ab9e90138556e7_c5b386b7a6348a2f1ba70f2259fb827e  \\\n",
       "0                                                NaN                                    \n",
       "1                                                NaN                                    \n",
       "2                                                NaN                                    \n",
       "3                                                2.0                                    \n",
       "4                                                NaN                                    \n",
       "\n",
       "   model_path_count_c5b386b7a6348a2f1ba70f2259fb827e_56e6187b5b44989eea9322ccb9443036  \n",
       "0                                                NaN                                   \n",
       "1                                                NaN                                   \n",
       "2                                                NaN                                   \n",
       "3                                                NaN                                   \n",
       "4                                                NaN                                   \n",
       "\n",
       "[5 rows x 67 columns]"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 3. 页面/模块访问路径特征\n",
    "def gen_mb_navigation_path_features(df):\n",
    "    \"\"\"\n",
    "    生成页面和模块之间的跳转路径特征\n",
    "    核心思想: 从上一个页面(REFERRER_TITLE)跳转到当前页面(PAGE_TITLE)\n",
    "    \"\"\"\n",
    "    feature = df[[\"CUST_NO\"]].drop_duplicates(['CUST_NO']).copy().reset_index(drop=True)\n",
    "    \n",
    "    # 3.1 创建页面跳转路径\n",
    "    df_page = df[['CUST_NO', 'REFERRER_TITLE', 'PAGE_TITLE', 'MODEL_NAME',\n",
    "                   'date_days_to_now', \"date_weeks_to_now\", 'date_months_to_now']].copy()\n",
    "    \n",
    "    # 页面路径拼接: 从REFERRER_TITLE到PAGE_TITLE\n",
    "    df_page['page_path'] = df['REFERRER_TITLE'].astype(str) + '_' + df['PAGE_TITLE'].astype(str)\n",
    "    \n",
    "    # 3.2 创建模块跳转路径 (通过页面映射到模块)\n",
    "    # 建立页面到模块的映射字典\n",
    "    page2model_dict = dict(zip(df['PAGE_TITLE'], df['MODEL_NAME']))\n",
    "    \n",
    "    df_model = df_page.copy()\n",
    "    df_model['REFERRER_MODEL_NAME'] = df_model['REFERRER_TITLE'].map(page2model_dict)\n",
    "    df_model['model_path'] = df_model['REFERRER_MODEL_NAME'].astype(str) + '_' + df_model['MODEL_NAME'].astype(str)\n",
    "    \n",
    "    # 3.3 统计各种路径的访问次数\n",
    "    # 每日页面路径访问次数\n",
    "    df_page_by_day = df_page.groupby([\n",
    "        \"CUST_NO\", \"date_days_to_now\", \"date_weeks_to_now\", \"date_months_to_now\", 'page_path'\n",
    "    ])['page_path'].agg(['count']).reset_index()\n",
    "    \n",
    "    # 每日模块路径访问次数\n",
    "    df_model_by_day = df_model.groupby([\n",
    "        \"CUST_NO\", \"date_days_to_now\", \"date_weeks_to_now\", \"date_months_to_now\", 'model_path'\n",
    "    ])['model_path'].agg(['count']).reset_index()\n",
    "    \n",
    "    # 3.4 路径多样性特征\n",
    "    # 用户访问的不同页面路径数量\n",
    "    tmp = df_page.groupby('CUST_NO')['page_path'].agg([\n",
    "        ('unique_page_paths', 'nunique'),\n",
    "        ('total_page_visits', 'count')\n",
    "    ]).reset_index()\n",
    "    tmp['page_path_diversity'] = tmp['unique_page_paths'] / (tmp['total_page_visits'] + 1)\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 用户访问的不同模块路径数量\n",
    "    tmp = df_model.groupby('CUST_NO')['model_path'].agg([\n",
    "        ('unique_model_paths', 'nunique'),\n",
    "        ('total_model_visits', 'count')\n",
    "    ]).reset_index()\n",
    "    tmp['model_path_diversity'] = tmp['unique_model_paths'] / (tmp['total_model_visits'] + 1)\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 3.5 Top路径的访问频次统计\n",
    "    # 获取最常见的页面路径\n",
    "    top_page_paths = df_page['page_path'].value_counts().head(50).index.tolist()\n",
    "    for path in tqdm(top_page_paths[:30], desc='Top页面路径特征'):  # 取前30\n",
    "        tmp = df_page[df_page['page_path'] == path].groupby('CUST_NO').size().to_frame(\n",
    "            f'page_path_count_{path}'\n",
    "        ).reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 获取最常见的模块路径\n",
    "    top_model_paths = df_model['model_path'].value_counts().head(50).index.tolist()\n",
    "    for path in tqdm(top_model_paths[:30], desc='Top模块路径特征'):  # 取前30\n",
    "        tmp = df_model[df_model['model_path'] == path].groupby('CUST_NO').size().to_frame(\n",
    "            f'model_path_count_{path}'\n",
    "        ).reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    return feature\n",
    "\n",
    "mb_navigation_features = gen_mb_navigation_path_features(MB_PAGEVIEW_DTL_data)\n",
    "print(f\"✓ 导航路径特征数量: {mb_navigation_features.shape[1] - 1}\")\n",
    "mb_navigation_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "264bed28",
   "metadata": {},
   "source": [
    "## 第四部分: 页面/模块分组统计特征 (Page/Module Grouping Features)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "bb30ebfa",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "生成页面访问统计特征...\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "页面分组特征: 100%|██████████| 60/60 [00:01<00:00, 59.32it/s]\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "生成模块访问统计特征...\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "模块分组特征: 100%|██████████| 40/40 [00:00<00:00, 46.26it/s]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✓ 页面/模块分组特征数量: 300\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>page_dc127d306179477fef4f3a9378dc550b_count</th>\n",
       "      <th>page_dc127d306179477fef4f3a9378dc550b_visit_days</th>\n",
       "      <th>page_dc127d306179477fef4f3a9378dc550b_last_visit</th>\n",
       "      <th>page_a3efea933884689e89b46cadd9aa989e_count</th>\n",
       "      <th>page_a3efea933884689e89b46cadd9aa989e_visit_days</th>\n",
       "      <th>page_a3efea933884689e89b46cadd9aa989e_last_visit</th>\n",
       "      <th>page_796f39eb9f0d6e6ff63d327d96fedeb5_count</th>\n",
       "      <th>page_796f39eb9f0d6e6ff63d327d96fedeb5_visit_days</th>\n",
       "      <th>page_796f39eb9f0d6e6ff63d327d96fedeb5_last_visit</th>\n",
       "      <th>...</th>\n",
       "      <th>module_d166e7d89059242e8210b0b647206cb1_last_visit</th>\n",
       "      <th>module_2613588ce2fcc962c5511a4e3eb234ba_count</th>\n",
       "      <th>module_2613588ce2fcc962c5511a4e3eb234ba_unique_pages</th>\n",
       "      <th>module_2613588ce2fcc962c5511a4e3eb234ba_last_visit</th>\n",
       "      <th>module_1993162004f0f20fe0bcda9337e150b4_count</th>\n",
       "      <th>module_1993162004f0f20fe0bcda9337e150b4_unique_pages</th>\n",
       "      <th>module_1993162004f0f20fe0bcda9337e150b4_last_visit</th>\n",
       "      <th>module_37d09e3760ce813af229ff907157fedf_count</th>\n",
       "      <th>module_37d09e3760ce813af229ff907157fedf_unique_pages</th>\n",
       "      <th>module_37d09e3760ce813af229ff907157fedf_last_visit</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>864a14a62ffffbc4741d365ea5a08278</td>\n",
       "      <td>21.0</td>\n",
       "      <td>12.0</td>\n",
       "      <td>66.0</td>\n",
       "      <td>16.0</td>\n",
       "      <td>12.0</td>\n",
       "      <td>66.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>74dfe9a67327540d1f427b40e85d49c7</td>\n",
       "      <td>48.0</td>\n",
       "      <td>23.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>9.0</td>\n",
       "      <td>2.0</td>\n",
       "      <td>67.0</td>\n",
       "      <td>33.0</td>\n",
       "      <td>22.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>078f92ffdfdfe07bb958c10c01d0733c</td>\n",
       "      <td>77.0</td>\n",
       "      <td>26.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>39.0</td>\n",
       "      <td>25.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>c1d381f50813111b4f1f8d3b124c369f</td>\n",
       "      <td>35.0</td>\n",
       "      <td>21.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>3f60ef34ad3853819e4269469280177d</td>\n",
       "      <td>103.0</td>\n",
       "      <td>32.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>3.0</td>\n",
       "      <td>2.0</td>\n",
       "      <td>18.0</td>\n",
       "      <td>46.0</td>\n",
       "      <td>32.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>5 rows × 301 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  \\\n",
       "0  864a14a62ffffbc4741d365ea5a08278   \n",
       "1  74dfe9a67327540d1f427b40e85d49c7   \n",
       "2  078f92ffdfdfe07bb958c10c01d0733c   \n",
       "3  c1d381f50813111b4f1f8d3b124c369f   \n",
       "4  3f60ef34ad3853819e4269469280177d   \n",
       "\n",
       "   page_dc127d306179477fef4f3a9378dc550b_count  \\\n",
       "0                                         21.0   \n",
       "1                                         48.0   \n",
       "2                                         77.0   \n",
       "3                                         35.0   \n",
       "4                                        103.0   \n",
       "\n",
       "   page_dc127d306179477fef4f3a9378dc550b_visit_days  \\\n",
       "0                                              12.0   \n",
       "1                                              23.0   \n",
       "2                                              26.0   \n",
       "3                                              21.0   \n",
       "4                                              32.0   \n",
       "\n",
       "   page_dc127d306179477fef4f3a9378dc550b_last_visit  \\\n",
       "0                                              66.0   \n",
       "1                                               0.0   \n",
       "2                                               0.0   \n",
       "3                                               4.0   \n",
       "4                                               1.0   \n",
       "\n",
       "   page_a3efea933884689e89b46cadd9aa989e_count  \\\n",
       "0                                         16.0   \n",
       "1                                          9.0   \n",
       "2                                          NaN   \n",
       "3                                          NaN   \n",
       "4                                          3.0   \n",
       "\n",
       "   page_a3efea933884689e89b46cadd9aa989e_visit_days  \\\n",
       "0                                              12.0   \n",
       "1                                               2.0   \n",
       "2                                               NaN   \n",
       "3                                               NaN   \n",
       "4                                               2.0   \n",
       "\n",
       "   page_a3efea933884689e89b46cadd9aa989e_last_visit  \\\n",
       "0                                              66.0   \n",
       "1                                              67.0   \n",
       "2                                               NaN   \n",
       "3                                               NaN   \n",
       "4                                              18.0   \n",
       "\n",
       "   page_796f39eb9f0d6e6ff63d327d96fedeb5_count  \\\n",
       "0                                          NaN   \n",
       "1                                         33.0   \n",
       "2                                         39.0   \n",
       "3                                          NaN   \n",
       "4                                         46.0   \n",
       "\n",
       "   page_796f39eb9f0d6e6ff63d327d96fedeb5_visit_days  \\\n",
       "0                                               NaN   \n",
       "1                                              22.0   \n",
       "2                                              25.0   \n",
       "3                                               NaN   \n",
       "4                                              32.0   \n",
       "\n",
       "   page_796f39eb9f0d6e6ff63d327d96fedeb5_last_visit  ...  \\\n",
       "0                                               NaN  ...   \n",
       "1                                               0.0  ...   \n",
       "2                                               0.0  ...   \n",
       "3                                               NaN  ...   \n",
       "4                                               1.0  ...   \n",
       "\n",
       "   module_d166e7d89059242e8210b0b647206cb1_last_visit  \\\n",
       "0                                                NaN    \n",
       "1                                                NaN    \n",
       "2                                                NaN    \n",
       "3                                                NaN    \n",
       "4                                                NaN    \n",
       "\n",
       "   module_2613588ce2fcc962c5511a4e3eb234ba_count  \\\n",
       "0                                            NaN   \n",
       "1                                            NaN   \n",
       "2                                            NaN   \n",
       "3                                            NaN   \n",
       "4                                            NaN   \n",
       "\n",
       "   module_2613588ce2fcc962c5511a4e3eb234ba_unique_pages  \\\n",
       "0                                                NaN      \n",
       "1                                                NaN      \n",
       "2                                                NaN      \n",
       "3                                                NaN      \n",
       "4                                                NaN      \n",
       "\n",
       "   module_2613588ce2fcc962c5511a4e3eb234ba_last_visit  \\\n",
       "0                                                NaN    \n",
       "1                                                NaN    \n",
       "2                                                NaN    \n",
       "3                                                NaN    \n",
       "4                                                NaN    \n",
       "\n",
       "   module_1993162004f0f20fe0bcda9337e150b4_count  \\\n",
       "0                                            NaN   \n",
       "1                                            NaN   \n",
       "2                                            NaN   \n",
       "3                                            NaN   \n",
       "4                                            NaN   \n",
       "\n",
       "   module_1993162004f0f20fe0bcda9337e150b4_unique_pages  \\\n",
       "0                                                NaN      \n",
       "1                                                NaN      \n",
       "2                                                NaN      \n",
       "3                                                NaN      \n",
       "4                                                NaN      \n",
       "\n",
       "   module_1993162004f0f20fe0bcda9337e150b4_last_visit  \\\n",
       "0                                                NaN    \n",
       "1                                                NaN    \n",
       "2                                                NaN    \n",
       "3                                                NaN    \n",
       "4                                                NaN    \n",
       "\n",
       "   module_37d09e3760ce813af229ff907157fedf_count  \\\n",
       "0                                            NaN   \n",
       "1                                            NaN   \n",
       "2                                            NaN   \n",
       "3                                            NaN   \n",
       "4                                            NaN   \n",
       "\n",
       "   module_37d09e3760ce813af229ff907157fedf_unique_pages  \\\n",
       "0                                                NaN      \n",
       "1                                                NaN      \n",
       "2                                                NaN      \n",
       "3                                                NaN      \n",
       "4                                                NaN      \n",
       "\n",
       "   module_37d09e3760ce813af229ff907157fedf_last_visit  \n",
       "0                                                NaN   \n",
       "1                                                NaN   \n",
       "2                                                NaN   \n",
       "3                                                NaN   \n",
       "4                                                NaN   \n",
       "\n",
       "[5 rows x 301 columns]"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 4. 页面/模块分组统计特征\n",
    "def gen_mb_page_module_group_features(df):\n",
    "    \"\"\"\n",
    "    对每个页面和模块进行分组统计\n",
    "    包括: 访问次数、访问时间分布、访问频率等\n",
    "    \"\"\"\n",
    "    feature = df[[\"CUST_NO\"]].drop_duplicates(['CUST_NO']).copy().reset_index(drop=True)\n",
    "    \n",
    "    # 4.1 每个页面的访问统计\n",
    "    print(\"生成页面访问统计特征...\")\n",
    "    \n",
    "    # 获取Top页面\n",
    "    top_pages = df['PAGE_TITLE'].value_counts().head(80).index.tolist()\n",
    "    \n",
    "    # 对Top页面进行多维度统计\n",
    "    for page in tqdm(top_pages[:60], desc='页面分组特征'):  # 控制在60个以内\n",
    "        df_page = df[df['PAGE_TITLE'] == page]\n",
    "        \n",
    "        # 访问次数\n",
    "        tmp = df_page.groupby('CUST_NO').size().to_frame(f'page_{page}_count').reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "        \n",
    "        # 访问天数\n",
    "        tmp = df_page.groupby('CUST_NO')['date_days_to_now'].nunique().to_frame(\n",
    "            f'page_{page}_visit_days'\n",
    "        ).reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "        \n",
    "        # 最近访问距今天数\n",
    "        tmp = df_page.groupby('CUST_NO')['date_days_to_now'].min().to_frame(\n",
    "            f'page_{page}_last_visit'\n",
    "        ).reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 4.2 每个模块的访问统计\n",
    "    print(\"生成模块访问统计特征...\")\n",
    "    \n",
    "    # 获取Top模块\n",
    "    top_modules = df['MODEL_NAME'].value_counts().head(50).index.tolist()\n",
    "    \n",
    "    # 对Top模块进行多维度统计\n",
    "    for module in tqdm(top_modules[:40], desc='模块分组特征'):  # 控制在40个以内\n",
    "        df_module = df[df['MODEL_NAME'] == module]\n",
    "        \n",
    "        # 访问次数\n",
    "        tmp = df_module.groupby('CUST_NO').size().to_frame(f'module_{module}_count').reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "        \n",
    "        # 访问的不同页面数\n",
    "        tmp = df_module.groupby('CUST_NO')['PAGE_TITLE'].nunique().to_frame(\n",
    "            f'module_{module}_unique_pages'\n",
    "        ).reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "        \n",
    "        # 最近访问距今天数\n",
    "        tmp = df_module.groupby('CUST_NO')['date_days_to_now'].min().to_frame(\n",
    "            f'module_{module}_last_visit'\n",
    "        ).reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    return feature\n",
    "\n",
    "mb_page_module_features = gen_mb_page_module_group_features(MB_PAGEVIEW_DTL_data)\n",
    "print(f\"✓ 页面/模块分组特征数量: {mb_page_module_features.shape[1] - 1}\")\n",
    "mb_page_module_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "95dcdd38",
   "metadata": {},
   "source": [
    "## 第五部分: 时间段访问行为特征 (Time Period Behavior Features)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "7685b1e0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✓ 时间段行为特征数量: 28\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>month_0_visit_count</th>\n",
       "      <th>month_0_page_nunique</th>\n",
       "      <th>month_0_module_nunique</th>\n",
       "      <th>month_0_active_days</th>\n",
       "      <th>month_1_visit_count</th>\n",
       "      <th>month_1_page_nunique</th>\n",
       "      <th>month_1_module_nunique</th>\n",
       "      <th>month_1_active_days</th>\n",
       "      <th>month_2_visit_count</th>\n",
       "      <th>...</th>\n",
       "      <th>weekday_weekend_ratio</th>\n",
       "      <th>month_start_visit_count</th>\n",
       "      <th>month_end_visit_count</th>\n",
       "      <th>dayofweek_0_count</th>\n",
       "      <th>dayofweek_1_count</th>\n",
       "      <th>dayofweek_2_count</th>\n",
       "      <th>dayofweek_3_count</th>\n",
       "      <th>dayofweek_4_count</th>\n",
       "      <th>dayofweek_5_count</th>\n",
       "      <th>dayofweek_6_count</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>864a14a62ffffbc4741d365ea5a08278</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>56.0</td>\n",
       "      <td>...</td>\n",
       "      <td>10.400000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>7.0</td>\n",
       "      <td>15.0</td>\n",
       "      <td>11.0</td>\n",
       "      <td>13.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>74dfe9a67327540d1f427b40e85d49c7</td>\n",
       "      <td>58.0</td>\n",
       "      <td>5.0</td>\n",
       "      <td>5.0</td>\n",
       "      <td>11.0</td>\n",
       "      <td>36.0</td>\n",
       "      <td>5.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>39.0</td>\n",
       "      <td>...</td>\n",
       "      <td>2.828571</td>\n",
       "      <td>4.0</td>\n",
       "      <td>11.0</td>\n",
       "      <td>34.0</td>\n",
       "      <td>26.0</td>\n",
       "      <td>20.0</td>\n",
       "      <td>15.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>30.0</td>\n",
       "      <td>4.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>078f92ffdfdfe07bb958c10c01d0733c</td>\n",
       "      <td>104.0</td>\n",
       "      <td>15.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>12.0</td>\n",
       "      <td>36.0</td>\n",
       "      <td>7.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>55.0</td>\n",
       "      <td>...</td>\n",
       "      <td>2.920000</td>\n",
       "      <td>8.0</td>\n",
       "      <td>5.0</td>\n",
       "      <td>37.0</td>\n",
       "      <td>11.0</td>\n",
       "      <td>29.0</td>\n",
       "      <td>32.0</td>\n",
       "      <td>37.0</td>\n",
       "      <td>25.0</td>\n",
       "      <td>24.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>c1d381f50813111b4f1f8d3b124c369f</td>\n",
       "      <td>46.0</td>\n",
       "      <td>7.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>8.0</td>\n",
       "      <td>34.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>3.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>59.0</td>\n",
       "      <td>...</td>\n",
       "      <td>6.368421</td>\n",
       "      <td>4.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>26.0</td>\n",
       "      <td>38.0</td>\n",
       "      <td>30.0</td>\n",
       "      <td>19.0</td>\n",
       "      <td>8.0</td>\n",
       "      <td>7.0</td>\n",
       "      <td>11.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>3f60ef34ad3853819e4269469280177d</td>\n",
       "      <td>96.0</td>\n",
       "      <td>17.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>12.0</td>\n",
       "      <td>120.0</td>\n",
       "      <td>24.0</td>\n",
       "      <td>8.0</td>\n",
       "      <td>12.0</td>\n",
       "      <td>115.0</td>\n",
       "      <td>...</td>\n",
       "      <td>5.148148</td>\n",
       "      <td>11.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>20.0</td>\n",
       "      <td>40.0</td>\n",
       "      <td>93.0</td>\n",
       "      <td>62.0</td>\n",
       "      <td>63.0</td>\n",
       "      <td>40.0</td>\n",
       "      <td>13.0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>5 rows × 29 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  month_0_visit_count  \\\n",
       "0  864a14a62ffffbc4741d365ea5a08278                  NaN   \n",
       "1  74dfe9a67327540d1f427b40e85d49c7                 58.0   \n",
       "2  078f92ffdfdfe07bb958c10c01d0733c                104.0   \n",
       "3  c1d381f50813111b4f1f8d3b124c369f                 46.0   \n",
       "4  3f60ef34ad3853819e4269469280177d                 96.0   \n",
       "\n",
       "   month_0_page_nunique  month_0_module_nunique  month_0_active_days  \\\n",
       "0                   NaN                     NaN                  NaN   \n",
       "1                   5.0                     5.0                 11.0   \n",
       "2                  15.0                     6.0                 12.0   \n",
       "3                   7.0                     4.0                  8.0   \n",
       "4                  17.0                     6.0                 12.0   \n",
       "\n",
       "   month_1_visit_count  month_1_page_nunique  month_1_module_nunique  \\\n",
       "0                  NaN                   NaN                     NaN   \n",
       "1                 36.0                   5.0                     4.0   \n",
       "2                 36.0                   7.0                     4.0   \n",
       "3                 34.0                   6.0                     3.0   \n",
       "4                120.0                  24.0                     8.0   \n",
       "\n",
       "   month_1_active_days  month_2_visit_count  ...  weekday_weekend_ratio  \\\n",
       "0                  NaN                 56.0  ...              10.400000   \n",
       "1                  6.0                 39.0  ...               2.828571   \n",
       "2                  6.0                 55.0  ...               2.920000   \n",
       "3                  6.0                 59.0  ...               6.368421   \n",
       "4                 12.0                115.0  ...               5.148148   \n",
       "\n",
       "   month_start_visit_count  month_end_visit_count  dayofweek_0_count  \\\n",
       "0                      NaN                    NaN                7.0   \n",
       "1                      4.0                   11.0               34.0   \n",
       "2                      8.0                    5.0               37.0   \n",
       "3                      4.0                    NaN               26.0   \n",
       "4                     11.0                    NaN               20.0   \n",
       "\n",
       "   dayofweek_1_count  dayofweek_2_count  dayofweek_3_count  dayofweek_4_count  \\\n",
       "0               15.0               11.0               13.0                6.0   \n",
       "1               26.0               20.0               15.0                4.0   \n",
       "2               11.0               29.0               32.0               37.0   \n",
       "3               38.0               30.0               19.0                8.0   \n",
       "4               40.0               93.0               62.0               63.0   \n",
       "\n",
       "   dayofweek_5_count  dayofweek_6_count  \n",
       "0                4.0                NaN  \n",
       "1               30.0                4.0  \n",
       "2               25.0               24.0  \n",
       "3                7.0               11.0  \n",
       "4               40.0               13.0  \n",
       "\n",
       "[5 rows x 29 columns]"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 5. 时间段访问行为特征\n",
    "def gen_mb_time_period_features(df):\n",
    "    \"\"\"\n",
    "    按不同时间段(月/周/工作日/周末等)统计访问行为\n",
    "    \"\"\"\n",
    "    feature = df[[\"CUST_NO\"]].drop_duplicates(['CUST_NO']).copy().reset_index(drop=True)\n",
    "    \n",
    "    # 5.1 按月份统计\n",
    "    for month in [0, 1, 2]:\n",
    "        df_month = df[df['date_months_to_now'] == month]\n",
    "        \n",
    "        # 该月访问次数\n",
    "        tmp = df_month.groupby('CUST_NO').size().to_frame(f'month_{month}_visit_count').reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "        \n",
    "        # 该月访问页面数\n",
    "        tmp = df_month.groupby('CUST_NO')['PAGE_TITLE'].nunique().to_frame(\n",
    "            f'month_{month}_page_nunique'\n",
    "        ).reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "        \n",
    "        # 该月访问模块数\n",
    "        tmp = df_month.groupby('CUST_NO')['MODEL_NAME'].nunique().to_frame(\n",
    "            f'month_{month}_module_nunique'\n",
    "        ).reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "        \n",
    "        # 该月访问天数\n",
    "        tmp = df_month.groupby('CUST_NO')['date_days_to_now'].nunique().to_frame(\n",
    "            f'month_{month}_active_days'\n",
    "        ).reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 5.2 工作日 vs 周末\n",
    "    df_weekday = df[df['operation_is_weekend'] == 0]\n",
    "    df_weekend = df[df['operation_is_weekend'] == 1]\n",
    "    \n",
    "    # 工作日访问统计\n",
    "    tmp = df_weekday.groupby('CUST_NO').agg({\n",
    "        'PAGE_TITLE': ['count', 'nunique'],\n",
    "        'MODEL_NAME': 'nunique'\n",
    "    })\n",
    "    tmp.columns = ['weekday_visit_count', 'weekday_page_nunique', 'weekday_module_nunique']\n",
    "    tmp = tmp.reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 周末访问统计\n",
    "    tmp = df_weekend.groupby('CUST_NO').agg({\n",
    "        'PAGE_TITLE': ['count', 'nunique'],\n",
    "        'MODEL_NAME': 'nunique'\n",
    "    })\n",
    "    tmp.columns = ['weekend_visit_count', 'weekend_page_nunique', 'weekend_module_nunique']\n",
    "    tmp = tmp.reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 工作日/周末访问比例\n",
    "    feature['weekday_weekend_ratio'] = feature['weekday_visit_count'] / (feature['weekend_visit_count'] + 1)\n",
    "    \n",
    "    # 5.3 月初/月末行为\n",
    "    df_month_start = df[df['operation_is_month_start'] == 1]\n",
    "    df_month_end = df[df['operation_is_month_end'] == 1]\n",
    "    \n",
    "    # 月初访问次数\n",
    "    tmp = df_month_start.groupby('CUST_NO').size().to_frame('month_start_visit_count').reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 月末访问次数\n",
    "    tmp = df_month_end.groupby('CUST_NO').size().to_frame('month_end_visit_count').reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 5.4 按周几统计\n",
    "    for dayofweek in range(7):\n",
    "        df_day = df[df['operation_dayofweek'] == dayofweek]\n",
    "        tmp = df_day.groupby('CUST_NO').size().to_frame(f'dayofweek_{dayofweek}_count').reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    return feature\n",
    "\n",
    "mb_time_period_features = gen_mb_time_period_features(MB_PAGEVIEW_DTL_data)\n",
    "print(f\"✓ 时间段行为特征数量: {mb_time_period_features.shape[1] - 1}\")\n",
    "mb_time_period_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "374590b2",
   "metadata": {},
   "source": [
    "## 第六部分: 用户访问习惯特征 (User Behavior Pattern Features)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "bc641464",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✓ 用户习惯特征数量: 24\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>total_visit_count</th>\n",
       "      <th>total_page_nunique</th>\n",
       "      <th>total_module_nunique</th>\n",
       "      <th>total_referrer_nunique</th>\n",
       "      <th>visit_days_min</th>\n",
       "      <th>visit_days_max</th>\n",
       "      <th>visit_days_mean</th>\n",
       "      <th>visit_days_std</th>\n",
       "      <th>visit_days_span</th>\n",
       "      <th>...</th>\n",
       "      <th>daily_visits_std</th>\n",
       "      <th>daily_visits_skew</th>\n",
       "      <th>daily_visits_kurt</th>\n",
       "      <th>top1_page_ratio</th>\n",
       "      <th>module_switch_count</th>\n",
       "      <th>module_switch_ratio</th>\n",
       "      <th>active_days</th>\n",
       "      <th>active_days_ratio</th>\n",
       "      <th>first_visit_days</th>\n",
       "      <th>last_visit_days</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>864a14a62ffffbc4741d365ea5a08278</td>\n",
       "      <td>56</td>\n",
       "      <td>6</td>\n",
       "      <td>3</td>\n",
       "      <td>7</td>\n",
       "      <td>66</td>\n",
       "      <td>89</td>\n",
       "      <td>76.982143</td>\n",
       "      <td>6.900569</td>\n",
       "      <td>23</td>\n",
       "      <td>...</td>\n",
       "      <td>2.774341</td>\n",
       "      <td>2.069584</td>\n",
       "      <td>4.120448</td>\n",
       "      <td>0.375000</td>\n",
       "      <td>5</td>\n",
       "      <td>0.089286</td>\n",
       "      <td>12</td>\n",
       "      <td>0.500000</td>\n",
       "      <td>66</td>\n",
       "      <td>89</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>74dfe9a67327540d1f427b40e85d49c7</td>\n",
       "      <td>133</td>\n",
       "      <td>11</td>\n",
       "      <td>5</td>\n",
       "      <td>12</td>\n",
       "      <td>0</td>\n",
       "      <td>90</td>\n",
       "      <td>42.105263</td>\n",
       "      <td>28.971842</td>\n",
       "      <td>90</td>\n",
       "      <td>...</td>\n",
       "      <td>3.044466</td>\n",
       "      <td>1.507036</td>\n",
       "      <td>1.575973</td>\n",
       "      <td>0.360902</td>\n",
       "      <td>91</td>\n",
       "      <td>0.684211</td>\n",
       "      <td>23</td>\n",
       "      <td>0.252747</td>\n",
       "      <td>0</td>\n",
       "      <td>90</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>078f92ffdfdfe07bb958c10c01d0733c</td>\n",
       "      <td>195</td>\n",
       "      <td>18</td>\n",
       "      <td>7</td>\n",
       "      <td>16</td>\n",
       "      <td>0</td>\n",
       "      <td>88</td>\n",
       "      <td>37.794872</td>\n",
       "      <td>31.508287</td>\n",
       "      <td>88</td>\n",
       "      <td>...</td>\n",
       "      <td>3.569314</td>\n",
       "      <td>1.606666</td>\n",
       "      <td>3.136583</td>\n",
       "      <td>0.394872</td>\n",
       "      <td>137</td>\n",
       "      <td>0.702564</td>\n",
       "      <td>26</td>\n",
       "      <td>0.292135</td>\n",
       "      <td>0</td>\n",
       "      <td>88</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>c1d381f50813111b4f1f8d3b124c369f</td>\n",
       "      <td>139</td>\n",
       "      <td>13</td>\n",
       "      <td>7</td>\n",
       "      <td>14</td>\n",
       "      <td>4</td>\n",
       "      <td>90</td>\n",
       "      <td>48.287770</td>\n",
       "      <td>26.873899</td>\n",
       "      <td>86</td>\n",
       "      <td>...</td>\n",
       "      <td>5.103687</td>\n",
       "      <td>2.237539</td>\n",
       "      <td>5.979316</td>\n",
       "      <td>0.302158</td>\n",
       "      <td>80</td>\n",
       "      <td>0.575540</td>\n",
       "      <td>21</td>\n",
       "      <td>0.241379</td>\n",
       "      <td>4</td>\n",
       "      <td>90</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>3f60ef34ad3853819e4269469280177d</td>\n",
       "      <td>331</td>\n",
       "      <td>38</td>\n",
       "      <td>11</td>\n",
       "      <td>36</td>\n",
       "      <td>1</td>\n",
       "      <td>90</td>\n",
       "      <td>50.277946</td>\n",
       "      <td>24.105729</td>\n",
       "      <td>89</td>\n",
       "      <td>...</td>\n",
       "      <td>8.334187</td>\n",
       "      <td>1.518758</td>\n",
       "      <td>1.176220</td>\n",
       "      <td>0.311178</td>\n",
       "      <td>247</td>\n",
       "      <td>0.746224</td>\n",
       "      <td>32</td>\n",
       "      <td>0.355556</td>\n",
       "      <td>1</td>\n",
       "      <td>90</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>5 rows × 25 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  total_visit_count  total_page_nunique  \\\n",
       "0  864a14a62ffffbc4741d365ea5a08278                 56                   6   \n",
       "1  74dfe9a67327540d1f427b40e85d49c7                133                  11   \n",
       "2  078f92ffdfdfe07bb958c10c01d0733c                195                  18   \n",
       "3  c1d381f50813111b4f1f8d3b124c369f                139                  13   \n",
       "4  3f60ef34ad3853819e4269469280177d                331                  38   \n",
       "\n",
       "   total_module_nunique  total_referrer_nunique  visit_days_min  \\\n",
       "0                     3                       7              66   \n",
       "1                     5                      12               0   \n",
       "2                     7                      16               0   \n",
       "3                     7                      14               4   \n",
       "4                    11                      36               1   \n",
       "\n",
       "   visit_days_max  visit_days_mean  visit_days_std  visit_days_span  ...  \\\n",
       "0              89        76.982143        6.900569               23  ...   \n",
       "1              90        42.105263       28.971842               90  ...   \n",
       "2              88        37.794872       31.508287               88  ...   \n",
       "3              90        48.287770       26.873899               86  ...   \n",
       "4              90        50.277946       24.105729               89  ...   \n",
       "\n",
       "   daily_visits_std  daily_visits_skew  daily_visits_kurt  top1_page_ratio  \\\n",
       "0          2.774341           2.069584           4.120448         0.375000   \n",
       "1          3.044466           1.507036           1.575973         0.360902   \n",
       "2          3.569314           1.606666           3.136583         0.394872   \n",
       "3          5.103687           2.237539           5.979316         0.302158   \n",
       "4          8.334187           1.518758           1.176220         0.311178   \n",
       "\n",
       "   module_switch_count  module_switch_ratio  active_days  active_days_ratio  \\\n",
       "0                    5             0.089286           12           0.500000   \n",
       "1                   91             0.684211           23           0.252747   \n",
       "2                  137             0.702564           26           0.292135   \n",
       "3                   80             0.575540           21           0.241379   \n",
       "4                  247             0.746224           32           0.355556   \n",
       "\n",
       "   first_visit_days  last_visit_days  \n",
       "0                66               89  \n",
       "1                 0               90  \n",
       "2                 0               88  \n",
       "3                 4               90  \n",
       "4                 1               90  \n",
       "\n",
       "[5 rows x 25 columns]"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 6. 用户访问习惯特征\n",
    "def gen_mb_user_habit_features(df):\n",
    "    \"\"\"\n",
    "    挖掘用户的访问习惯和偏好特征\n",
    "    \"\"\"\n",
    "    feature = df[[\"CUST_NO\"]].drop_duplicates(['CUST_NO']).copy().reset_index(drop=True)\n",
    "    \n",
    "    # 6.1 整体访问统计\n",
    "    tmp = df.groupby('CUST_NO').agg({\n",
    "        'PAGE_TITLE': ['count', 'nunique'],\n",
    "        'MODEL_NAME': 'nunique',\n",
    "        'REFERRER_TITLE': 'nunique',\n",
    "        'date_days_to_now': ['min', 'max', 'mean', 'std']\n",
    "    })\n",
    "    tmp.columns = [\n",
    "        'total_visit_count', 'total_page_nunique', 'total_module_nunique', \n",
    "        'total_referrer_nunique', 'visit_days_min', 'visit_days_max', \n",
    "        'visit_days_mean', 'visit_days_std'\n",
    "    ]\n",
    "    tmp = tmp.reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 访问跨度\n",
    "    feature['visit_days_span'] = feature['visit_days_max'] - feature['visit_days_min']\n",
    "    \n",
    "    # 访问集中度 (访问越集中,std越小)\n",
    "    feature['visit_concentration'] = 1 / (feature['visit_days_std'] + 1)\n",
    "    \n",
    "    # 平均每天访问的页面数\n",
    "    feature['avg_pages_per_day'] = feature['total_page_nunique'] / (feature['visit_days_span'] + 1)\n",
    "    \n",
    "    # 6.2 访问频率特征\n",
    "    # 每个用户每天的访问次数\n",
    "    df_daily_visits = df.groupby(['CUST_NO', 'date_days_to_now']).size().reset_index(name='daily_visits')\n",
    "    \n",
    "    tmp = df_daily_visits.groupby('CUST_NO')['daily_visits'].agg([\n",
    "        'mean', 'max', 'min', 'std', \n",
    "        ('skew', lambda x: x.skew()),\n",
    "        ('kurt', lambda x: x.kurt())\n",
    "    ])\n",
    "    tmp.columns = [f'daily_visits_{col}' for col in tmp.columns]\n",
    "    tmp = tmp.reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 6.3 页面浏览深度特征\n",
    "    # 用户最常访问的页面占比\n",
    "    df_page_freq = df.groupby(['CUST_NO', 'PAGE_TITLE']).size().reset_index(name='page_count')\n",
    "    df_top1_page = df_page_freq.sort_values(['CUST_NO', 'page_count'], ascending=[True, False])\n",
    "    df_top1_page = df_top1_page.groupby('CUST_NO').first().reset_index()\n",
    "    df_top1_page['top1_page_ratio'] = df_top1_page['page_count'] / df_page_freq.groupby('CUST_NO')['page_count'].sum().values\n",
    "    feature = feature.merge(df_top1_page[['CUST_NO', 'top1_page_ratio']], on='CUST_NO', how='left')\n",
    "    \n",
    "    # 6.4 模块切换频率\n",
    "    # 计算用户在不同模块之间的跳转次数\n",
    "    df_sorted = df.sort_values(['CUST_NO', 'OPERATION_DATE', 'date_days_to_now'])\n",
    "    df_sorted['next_module'] = df_sorted.groupby('CUST_NO')['MODEL_NAME'].shift(-1)\n",
    "    df_sorted['module_switch'] = (df_sorted['MODEL_NAME'] != df_sorted['next_module']).astype(int)\n",
    "    \n",
    "    tmp = df_sorted.groupby('CUST_NO')['module_switch'].agg(['sum', 'mean'])\n",
    "    tmp.columns = ['module_switch_count', 'module_switch_ratio']\n",
    "    tmp = tmp.reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 6.5 访问规律性特征\n",
    "    # 访问天数占总天数的比例\n",
    "    tmp = df.groupby('CUST_NO')['date_days_to_now'].nunique().to_frame('active_days').reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    feature['active_days_ratio'] = feature['active_days'] / (feature['visit_days_span'] + 1)\n",
    "    \n",
    "    # 6.6 首次和末次访问特征\n",
    "    tmp = df.groupby('CUST_NO').agg({\n",
    "        'date_days_to_now': ['min', 'max'],\n",
    "        'PAGE_TITLE': 'first',\n",
    "        'MODEL_NAME': 'last'\n",
    "    })\n",
    "    tmp.columns = ['first_visit_days', 'last_visit_days', 'first_page', 'last_module']\n",
    "    tmp = tmp.reset_index()\n",
    "    \n",
    "    # 首次访问距今天数\n",
    "    feature = feature.merge(tmp[['CUST_NO', 'first_visit_days', 'last_visit_days']], on='CUST_NO', how='left')\n",
    "    \n",
    "    return feature\n",
    "\n",
    "mb_user_habit_features = gen_mb_user_habit_features(MB_PAGEVIEW_DTL_data)\n",
    "print(f\"✓ 用户习惯特征数量: {mb_user_habit_features.shape[1] - 1}\")\n",
    "mb_user_habit_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e6e2ece3",
   "metadata": {},
   "source": [
    "## 第七部分: 趋势变化特征 (Trend Features)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "c817a1e7",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✓ 趋势变化特征数量: 33\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>m0_visit_count</th>\n",
       "      <th>m0_page_nunique</th>\n",
       "      <th>m0_module_nunique</th>\n",
       "      <th>m1_visit_count</th>\n",
       "      <th>m1_page_nunique</th>\n",
       "      <th>m1_module_nunique</th>\n",
       "      <th>m2_visit_count</th>\n",
       "      <th>m2_page_nunique</th>\n",
       "      <th>m2_module_nunique</th>\n",
       "      <th>...</th>\n",
       "      <th>week_5_count</th>\n",
       "      <th>week_6_count</th>\n",
       "      <th>week_7_count</th>\n",
       "      <th>week_8_count</th>\n",
       "      <th>week_9_count</th>\n",
       "      <th>week_10_count</th>\n",
       "      <th>week_11_count</th>\n",
       "      <th>recent_4weeks_std</th>\n",
       "      <th>recent_4weeks_mean</th>\n",
       "      <th>recent_4weeks_cv</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>864a14a62ffffbc4741d365ea5a08278</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>56.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>3.0</td>\n",
       "      <td>...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>7.0</td>\n",
       "      <td>29.0</td>\n",
       "      <td>9.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>74dfe9a67327540d1f427b40e85d49c7</td>\n",
       "      <td>58.0</td>\n",
       "      <td>5.0</td>\n",
       "      <td>5.0</td>\n",
       "      <td>36.0</td>\n",
       "      <td>5.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>39.0</td>\n",
       "      <td>9.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>...</td>\n",
       "      <td>10.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>8.0</td>\n",
       "      <td>14.0</td>\n",
       "      <td>15.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>4.0</td>\n",
       "      <td>4.932883</td>\n",
       "      <td>17.333333</td>\n",
       "      <td>0.269066</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>078f92ffdfdfe07bb958c10c01d0733c</td>\n",
       "      <td>104.0</td>\n",
       "      <td>15.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>36.0</td>\n",
       "      <td>7.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>55.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>10.0</td>\n",
       "      <td>13.0</td>\n",
       "      <td>8.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>12.0</td>\n",
       "      <td>18.0</td>\n",
       "      <td>19.096247</td>\n",
       "      <td>26.000000</td>\n",
       "      <td>0.707268</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>c1d381f50813111b4f1f8d3b124c369f</td>\n",
       "      <td>46.0</td>\n",
       "      <td>7.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>34.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>3.0</td>\n",
       "      <td>59.0</td>\n",
       "      <td>7.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>...</td>\n",
       "      <td>3.0</td>\n",
       "      <td>16.0</td>\n",
       "      <td>12.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>24.0</td>\n",
       "      <td>7.0</td>\n",
       "      <td>20.0</td>\n",
       "      <td>5.744563</td>\n",
       "      <td>11.500000</td>\n",
       "      <td>0.459565</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>3f60ef34ad3853819e4269469280177d</td>\n",
       "      <td>96.0</td>\n",
       "      <td>17.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>120.0</td>\n",
       "      <td>24.0</td>\n",
       "      <td>8.0</td>\n",
       "      <td>115.0</td>\n",
       "      <td>18.0</td>\n",
       "      <td>7.0</td>\n",
       "      <td>...</td>\n",
       "      <td>27.0</td>\n",
       "      <td>35.0</td>\n",
       "      <td>30.0</td>\n",
       "      <td>20.0</td>\n",
       "      <td>11.0</td>\n",
       "      <td>39.0</td>\n",
       "      <td>47.0</td>\n",
       "      <td>21.654484</td>\n",
       "      <td>22.750000</td>\n",
       "      <td>0.911768</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>5 rows × 34 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  m0_visit_count  m0_page_nunique  \\\n",
       "0  864a14a62ffffbc4741d365ea5a08278             NaN              NaN   \n",
       "1  74dfe9a67327540d1f427b40e85d49c7            58.0              5.0   \n",
       "2  078f92ffdfdfe07bb958c10c01d0733c           104.0             15.0   \n",
       "3  c1d381f50813111b4f1f8d3b124c369f            46.0              7.0   \n",
       "4  3f60ef34ad3853819e4269469280177d            96.0             17.0   \n",
       "\n",
       "   m0_module_nunique  m1_visit_count  m1_page_nunique  m1_module_nunique  \\\n",
       "0                NaN             NaN              NaN                NaN   \n",
       "1                5.0            36.0              5.0                4.0   \n",
       "2                6.0            36.0              7.0                4.0   \n",
       "3                4.0            34.0              6.0                3.0   \n",
       "4                6.0           120.0             24.0                8.0   \n",
       "\n",
       "   m2_visit_count  m2_page_nunique  m2_module_nunique  ...  week_5_count  \\\n",
       "0            56.0              6.0                3.0  ...           NaN   \n",
       "1            39.0              9.0                4.0  ...          10.0   \n",
       "2            55.0              6.0                4.0  ...           NaN   \n",
       "3            59.0              7.0                4.0  ...           3.0   \n",
       "4           115.0             18.0                7.0  ...          27.0   \n",
       "\n",
       "   week_6_count  week_7_count  week_8_count  week_9_count  week_10_count  \\\n",
       "0           NaN           NaN           NaN           7.0           29.0   \n",
       "1           4.0           8.0          14.0          15.0            NaN   \n",
       "2          10.0          13.0           8.0           NaN           12.0   \n",
       "3          16.0          12.0           NaN          24.0            7.0   \n",
       "4          35.0          30.0          20.0          11.0           39.0   \n",
       "\n",
       "   week_11_count  recent_4weeks_std  recent_4weeks_mean  recent_4weeks_cv  \n",
       "0            9.0                NaN                 NaN               NaN  \n",
       "1            4.0           4.932883           17.333333          0.269066  \n",
       "2           18.0          19.096247           26.000000          0.707268  \n",
       "3           20.0           5.744563           11.500000          0.459565  \n",
       "4           47.0          21.654484           22.750000          0.911768  \n",
       "\n",
       "[5 rows x 34 columns]"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 7. 趋势变化特征\n",
    "def gen_mb_trend_features(df):\n",
    "    \"\"\"\n",
    "    捕捉用户访问行为的趋势变化\n",
    "    比较不同月份之间的变化\n",
    "    \"\"\"\n",
    "    feature = df[[\"CUST_NO\"]].drop_duplicates(['CUST_NO']).copy().reset_index(drop=True)\n",
    "    \n",
    "    # 7.1 月度访问趋势\n",
    "    # 第0月(最近)、第1月(次近)、第2月(最早)的访问统计\n",
    "    month_stats = {}\n",
    "    for month in [0, 1, 2]:\n",
    "        df_month = df[df['date_months_to_now'] == month]\n",
    "        tmp = df_month.groupby('CUST_NO').agg({\n",
    "            'PAGE_TITLE': ['count', 'nunique'],\n",
    "            'MODEL_NAME': 'nunique'\n",
    "        })\n",
    "        tmp.columns = [f'm{month}_visit_count', f'm{month}_page_nunique', f'm{month}_module_nunique']\n",
    "        month_stats[month] = tmp.reset_index()\n",
    "    \n",
    "    # 合并月度统计\n",
    "    for month in [0, 1, 2]:\n",
    "        feature = feature.merge(month_stats[month], on='CUST_NO', how='left')\n",
    "    \n",
    "    # 7.2 月度变化率\n",
    "    # 最近月相对次近月的变化\n",
    "    feature['visit_count_change_m0_m1'] = (feature['m0_visit_count'] - feature['m1_visit_count']) / (feature['m1_visit_count'] + 1)\n",
    "    feature['page_nunique_change_m0_m1'] = (feature['m0_page_nunique'] - feature['m1_page_nunique']) / (feature['m1_page_nunique'] + 1)\n",
    "    \n",
    "    # 次近月相对最早月的变化\n",
    "    feature['visit_count_change_m1_m2'] = (feature['m1_visit_count'] - feature['m2_visit_count']) / (feature['m2_visit_count'] + 1)\n",
    "    feature['page_nunique_change_m1_m2'] = (feature['m1_page_nunique'] - feature['m2_page_nunique']) / (feature['m2_page_nunique'] + 1)\n",
    "    \n",
    "    # 最近月相对最早月的变化\n",
    "    feature['visit_count_change_m0_m2'] = (feature['m0_visit_count'] - feature['m2_visit_count']) / (feature['m2_visit_count'] + 1)\n",
    "    feature['page_nunique_change_m0_m2'] = (feature['m0_page_nunique'] - feature['m2_page_nunique']) / (feature['m2_page_nunique'] + 1)\n",
    "    \n",
    "    # 7.3 访问加速度(二阶差分)\n",
    "    feature['visit_acceleration'] = feature['visit_count_change_m0_m1'] - feature['visit_count_change_m1_m2']\n",
    "    \n",
    "    # 7.4 访问趋势(上升/下降/稳定)\n",
    "    feature['visit_trend_up'] = ((feature['m0_visit_count'] > feature['m1_visit_count']) & \n",
    "                                  (feature['m1_visit_count'] > feature['m2_visit_count'])).astype(int)\n",
    "    feature['visit_trend_down'] = ((feature['m0_visit_count'] < feature['m1_visit_count']) & \n",
    "                                    (feature['m1_visit_count'] < feature['m2_visit_count'])).astype(int)\n",
    "    \n",
    "    # 7.5 周度访问变化\n",
    "    week_stats = {}\n",
    "    for week in range(min(df['date_weeks_to_now'].max() + 1, 12)):  # 最多12周\n",
    "        df_week = df[df['date_weeks_to_now'] == week]\n",
    "        tmp = df_week.groupby('CUST_NO').size().to_frame(f'week_{week}_count').reset_index()\n",
    "        week_stats[week] = tmp\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 计算最近4周的变化系数\n",
    "    if len(week_stats) >= 4:\n",
    "        week_cols = [f'week_{i}_count' for i in range(4)]\n",
    "        feature['recent_4weeks_std'] = feature[week_cols].std(axis=1)\n",
    "        feature['recent_4weeks_mean'] = feature[week_cols].mean(axis=1)\n",
    "        feature['recent_4weeks_cv'] = feature['recent_4weeks_std'] / (feature['recent_4weeks_mean'] + 1)\n",
    "    \n",
    "    return feature\n",
    "\n",
    "mb_trend_features = gen_mb_trend_features(MB_PAGEVIEW_DTL_data)\n",
    "print(f\"✓ 趋势变化特征数量: {mb_trend_features.shape[1] - 1}\")\n",
    "mb_trend_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bafbb6f4",
   "metadata": {},
   "source": [
    "## 第八部分: 交叉统计特征 (Cross Features)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "e9c6b315",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✓ 交叉统计特征数量: 17\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>module_avg_pages_mean</th>\n",
       "      <th>module_avg_pages_max</th>\n",
       "      <th>module_avg_pages_min</th>\n",
       "      <th>module_avg_pages_std</th>\n",
       "      <th>daily_unique_pages_mean</th>\n",
       "      <th>daily_unique_pages_max</th>\n",
       "      <th>daily_unique_pages_std</th>\n",
       "      <th>daily_unique_modules_mean</th>\n",
       "      <th>daily_unique_modules_max</th>\n",
       "      <th>daily_unique_modules_std</th>\n",
       "      <th>dayofweek_visit_std</th>\n",
       "      <th>dayofweek_visit_mean</th>\n",
       "      <th>dayofweek_visit_cv</th>\n",
       "      <th>m0_daily_module_diversity</th>\n",
       "      <th>m1_daily_module_diversity</th>\n",
       "      <th>m2_daily_module_diversity</th>\n",
       "      <th>top_module_ratio</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>864a14a62ffffbc4741d365ea5a08278</td>\n",
       "      <td>2.000000</td>\n",
       "      <td>4</td>\n",
       "      <td>1</td>\n",
       "      <td>1.732051</td>\n",
       "      <td>3.250000</td>\n",
       "      <td>6</td>\n",
       "      <td>0.866025</td>\n",
       "      <td>1.166667</td>\n",
       "      <td>3</td>\n",
       "      <td>0.577350</td>\n",
       "      <td>4.320494</td>\n",
       "      <td>9.333333</td>\n",
       "      <td>0.418112</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>1.166667</td>\n",
       "      <td>0.964286</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>74dfe9a67327540d1f427b40e85d49c7</td>\n",
       "      <td>2.200000</td>\n",
       "      <td>7</td>\n",
       "      <td>1</td>\n",
       "      <td>2.683282</td>\n",
       "      <td>3.739130</td>\n",
       "      <td>7</td>\n",
       "      <td>1.053884</td>\n",
       "      <td>3.304348</td>\n",
       "      <td>4</td>\n",
       "      <td>0.702902</td>\n",
       "      <td>11.986103</td>\n",
       "      <td>19.000000</td>\n",
       "      <td>0.599305</td>\n",
       "      <td>3.454545</td>\n",
       "      <td>3.500000</td>\n",
       "      <td>2.833333</td>\n",
       "      <td>0.488722</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>078f92ffdfdfe07bb958c10c01d0733c</td>\n",
       "      <td>2.571429</td>\n",
       "      <td>7</td>\n",
       "      <td>1</td>\n",
       "      <td>2.070197</td>\n",
       "      <td>4.500000</td>\n",
       "      <td>9</td>\n",
       "      <td>1.303840</td>\n",
       "      <td>3.461538</td>\n",
       "      <td>5</td>\n",
       "      <td>0.760567</td>\n",
       "      <td>9.063270</td>\n",
       "      <td>27.857143</td>\n",
       "      <td>0.314074</td>\n",
       "      <td>3.750000</td>\n",
       "      <td>2.833333</td>\n",
       "      <td>3.500000</td>\n",
       "      <td>0.528205</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>c1d381f50813111b4f1f8d3b124c369f</td>\n",
       "      <td>1.857143</td>\n",
       "      <td>4</td>\n",
       "      <td>1</td>\n",
       "      <td>1.069045</td>\n",
       "      <td>3.904762</td>\n",
       "      <td>7</td>\n",
       "      <td>1.179185</td>\n",
       "      <td>2.238095</td>\n",
       "      <td>4</td>\n",
       "      <td>0.624881</td>\n",
       "      <td>11.936339</td>\n",
       "      <td>19.857143</td>\n",
       "      <td>0.572290</td>\n",
       "      <td>2.250000</td>\n",
       "      <td>2.166667</td>\n",
       "      <td>2.285714</td>\n",
       "      <td>0.640288</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>3f60ef34ad3853819e4269469280177d</td>\n",
       "      <td>3.454545</td>\n",
       "      <td>15</td>\n",
       "      <td>1</td>\n",
       "      <td>4.156047</td>\n",
       "      <td>6.125000</td>\n",
       "      <td>16</td>\n",
       "      <td>3.480360</td>\n",
       "      <td>4.250000</td>\n",
       "      <td>7</td>\n",
       "      <td>0.915811</td>\n",
       "      <td>27.626764</td>\n",
       "      <td>47.285714</td>\n",
       "      <td>0.572152</td>\n",
       "      <td>3.916667</td>\n",
       "      <td>4.083333</td>\n",
       "      <td>5.000000</td>\n",
       "      <td>0.471299</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  module_avg_pages_mean  \\\n",
       "0  864a14a62ffffbc4741d365ea5a08278               2.000000   \n",
       "1  74dfe9a67327540d1f427b40e85d49c7               2.200000   \n",
       "2  078f92ffdfdfe07bb958c10c01d0733c               2.571429   \n",
       "3  c1d381f50813111b4f1f8d3b124c369f               1.857143   \n",
       "4  3f60ef34ad3853819e4269469280177d               3.454545   \n",
       "\n",
       "   module_avg_pages_max  module_avg_pages_min  module_avg_pages_std  \\\n",
       "0                     4                     1              1.732051   \n",
       "1                     7                     1              2.683282   \n",
       "2                     7                     1              2.070197   \n",
       "3                     4                     1              1.069045   \n",
       "4                    15                     1              4.156047   \n",
       "\n",
       "   daily_unique_pages_mean  daily_unique_pages_max  daily_unique_pages_std  \\\n",
       "0                 3.250000                       6                0.866025   \n",
       "1                 3.739130                       7                1.053884   \n",
       "2                 4.500000                       9                1.303840   \n",
       "3                 3.904762                       7                1.179185   \n",
       "4                 6.125000                      16                3.480360   \n",
       "\n",
       "   daily_unique_modules_mean  daily_unique_modules_max  \\\n",
       "0                   1.166667                         3   \n",
       "1                   3.304348                         4   \n",
       "2                   3.461538                         5   \n",
       "3                   2.238095                         4   \n",
       "4                   4.250000                         7   \n",
       "\n",
       "   daily_unique_modules_std  dayofweek_visit_std  dayofweek_visit_mean  \\\n",
       "0                  0.577350             4.320494              9.333333   \n",
       "1                  0.702902            11.986103             19.000000   \n",
       "2                  0.760567             9.063270             27.857143   \n",
       "3                  0.624881            11.936339             19.857143   \n",
       "4                  0.915811            27.626764             47.285714   \n",
       "\n",
       "   dayofweek_visit_cv  m0_daily_module_diversity  m1_daily_module_diversity  \\\n",
       "0            0.418112                        NaN                        NaN   \n",
       "1            0.599305                   3.454545                   3.500000   \n",
       "2            0.314074                   3.750000                   2.833333   \n",
       "3            0.572290                   2.250000                   2.166667   \n",
       "4            0.572152                   3.916667                   4.083333   \n",
       "\n",
       "   m2_daily_module_diversity  top_module_ratio  \n",
       "0                   1.166667          0.964286  \n",
       "1                   2.833333          0.488722  \n",
       "2                   3.500000          0.528205  \n",
       "3                   2.285714          0.640288  \n",
       "4                   5.000000          0.471299  "
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 8. 交叉统计特征\n",
    "def gen_mb_cross_features(df):\n",
    "    \"\"\"\n",
    "    页面、模块、时间等多维度交叉统计\n",
    "    \"\"\"\n",
    "    feature = df[[\"CUST_NO\"]].drop_duplicates(['CUST_NO']).copy().reset_index(drop=True)\n",
    "    \n",
    "    # 8.1 模块-页面交叉特征\n",
    "    # 每个模块下访问的平均页面数\n",
    "    tmp1 = df.groupby(['CUST_NO', 'MODEL_NAME'])['PAGE_TITLE'].nunique().reset_index()\n",
    "    tmp2 = tmp1.groupby('CUST_NO')['PAGE_TITLE'].agg(['mean', 'max', 'min', 'std'])\n",
    "    tmp2.columns = [f'module_avg_pages_{col}' for col in tmp2.columns]\n",
    "    tmp2 = tmp2.reset_index()\n",
    "    feature = feature.merge(tmp2, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 8.2 时间-页面交叉特征\n",
    "    # 每天访问的平均页面种类\n",
    "    tmp1 = df.groupby(['CUST_NO', 'date_days_to_now'])['PAGE_TITLE'].nunique().reset_index()\n",
    "    tmp2 = tmp1.groupby('CUST_NO')['PAGE_TITLE'].agg(['mean', 'max', 'std'])\n",
    "    tmp2.columns = [f'daily_unique_pages_{col}' for col in tmp2.columns]\n",
    "    tmp2 = tmp2.reset_index()\n",
    "    feature = feature.merge(tmp2, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 8.3 时间-模块交叉特征\n",
    "    # 每天访问的平均模块种类\n",
    "    tmp1 = df.groupby(['CUST_NO', 'date_days_to_now'])['MODEL_NAME'].nunique().reset_index()\n",
    "    tmp2 = tmp1.groupby('CUST_NO')['MODEL_NAME'].agg(['mean', 'max', 'std'])\n",
    "    tmp2.columns = [f'daily_unique_modules_{col}' for col in tmp2.columns]\n",
    "    tmp2 = tmp2.reset_index()\n",
    "    feature = feature.merge(tmp2, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 8.4 周几-访问量交叉\n",
    "    df_dayofweek_stats = df.groupby(['CUST_NO', 'operation_dayofweek']).size().reset_index(name='count')\n",
    "    tmp = df_dayofweek_stats.groupby('CUST_NO')['count'].agg(['std', 'mean'])\n",
    "    tmp.columns = ['dayofweek_visit_std', 'dayofweek_visit_mean']\n",
    "    tmp['dayofweek_visit_cv'] = tmp['dayofweek_visit_std'] / (tmp['dayofweek_visit_mean'] + 1)\n",
    "    tmp = tmp.reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 8.5 月份-模块多样性\n",
    "    for month in [0, 1, 2]:\n",
    "        df_month = df[df['date_months_to_now'] == month]\n",
    "        # 该月每天访问的模块种类\n",
    "        tmp1 = df_month.groupby(['CUST_NO', 'date_days_to_now'])['MODEL_NAME'].nunique().reset_index()\n",
    "        tmp2 = tmp1.groupby('CUST_NO')['MODEL_NAME'].mean().to_frame(f'm{month}_daily_module_diversity').reset_index()\n",
    "        feature = feature.merge(tmp2, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 8.6 比例类交叉特征\n",
    "    # 最常访问的模块占比\n",
    "    df_module_freq = df.groupby(['CUST_NO', 'MODEL_NAME']).size().reset_index(name='module_count')\n",
    "    df_top_module = df_module_freq.sort_values(['CUST_NO', 'module_count'], ascending=[True, False])\n",
    "    df_top_module = df_top_module.groupby('CUST_NO').first().reset_index()\n",
    "    df_total_module = df.groupby('CUST_NO').size().reset_index(name='total_count')\n",
    "    df_top_module = df_top_module.merge(df_total_module, on='CUST_NO')\n",
    "    df_top_module['top_module_ratio'] = df_top_module['module_count'] / df_top_module['total_count']\n",
    "    feature = feature.merge(df_top_module[['CUST_NO', 'top_module_ratio']], on='CUST_NO', how='left')\n",
    "    \n",
    "    return feature\n",
    "\n",
    "mb_cross_features = gen_mb_cross_features(MB_PAGEVIEW_DTL_data)\n",
    "print(f\"✓ 交叉统计特征数量: {mb_cross_features.shape[1] - 1}\")\n",
    "mb_cross_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "46e1c47e",
   "metadata": {},
   "source": [
    "## 第九部分: 序列行为特征 (Sequential Behavior Features)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "0c15cf53",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✓ 序列行为特征数量: 16\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>same_page_consecutive_count</th>\n",
       "      <th>same_page_consecutive_ratio</th>\n",
       "      <th>same_module_consecutive_count</th>\n",
       "      <th>same_module_consecutive_ratio</th>\n",
       "      <th>revisited_page_count</th>\n",
       "      <th>avg_page_visit_count</th>\n",
       "      <th>visit_interval_mean</th>\n",
       "      <th>visit_interval_max</th>\n",
       "      <th>visit_interval_min</th>\n",
       "      <th>visit_interval_std</th>\n",
       "      <th>total_sessions</th>\n",
       "      <th>session_visit_mean</th>\n",
       "      <th>session_visit_max</th>\n",
       "      <th>session_visit_std</th>\n",
       "      <th>recent_3_page_nunique</th>\n",
       "      <th>recent_3_module_nunique</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>864a14a62ffffbc4741d365ea5a08278</td>\n",
       "      <td>7</td>\n",
       "      <td>0.125000</td>\n",
       "      <td>51</td>\n",
       "      <td>0.910714</td>\n",
       "      <td>4.0</td>\n",
       "      <td>9.333333</td>\n",
       "      <td>0.418182</td>\n",
       "      <td>5.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>1.030838</td>\n",
       "      <td>6</td>\n",
       "      <td>8.000000</td>\n",
       "      <td>21</td>\n",
       "      <td>5.916080</td>\n",
       "      <td>3</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>74dfe9a67327540d1f427b40e85d49c7</td>\n",
       "      <td>28</td>\n",
       "      <td>0.210526</td>\n",
       "      <td>42</td>\n",
       "      <td>0.315789</td>\n",
       "      <td>8.0</td>\n",
       "      <td>12.090909</td>\n",
       "      <td>0.681818</td>\n",
       "      <td>11.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>1.982050</td>\n",
       "      <td>18</td>\n",
       "      <td>7.000000</td>\n",
       "      <td>17</td>\n",
       "      <td>4.163332</td>\n",
       "      <td>2</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>078f92ffdfdfe07bb958c10c01d0733c</td>\n",
       "      <td>33</td>\n",
       "      <td>0.169231</td>\n",
       "      <td>58</td>\n",
       "      <td>0.297436</td>\n",
       "      <td>9.0</td>\n",
       "      <td>10.833333</td>\n",
       "      <td>0.453608</td>\n",
       "      <td>13.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>1.682095</td>\n",
       "      <td>16</td>\n",
       "      <td>11.470588</td>\n",
       "      <td>40</td>\n",
       "      <td>10.648460</td>\n",
       "      <td>3</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>c1d381f50813111b4f1f8d3b124c369f</td>\n",
       "      <td>17</td>\n",
       "      <td>0.122302</td>\n",
       "      <td>59</td>\n",
       "      <td>0.424460</td>\n",
       "      <td>8.0</td>\n",
       "      <td>10.692308</td>\n",
       "      <td>0.623188</td>\n",
       "      <td>13.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>1.993430</td>\n",
       "      <td>13</td>\n",
       "      <td>9.928571</td>\n",
       "      <td>24</td>\n",
       "      <td>6.911131</td>\n",
       "      <td>3</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>3f60ef34ad3853819e4269469280177d</td>\n",
       "      <td>41</td>\n",
       "      <td>0.123867</td>\n",
       "      <td>84</td>\n",
       "      <td>0.253776</td>\n",
       "      <td>20.0</td>\n",
       "      <td>8.710526</td>\n",
       "      <td>0.269697</td>\n",
       "      <td>8.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>1.053287</td>\n",
       "      <td>21</td>\n",
       "      <td>15.045455</td>\n",
       "      <td>49</td>\n",
       "      <td>12.518471</td>\n",
       "      <td>3</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  same_page_consecutive_count  \\\n",
       "0  864a14a62ffffbc4741d365ea5a08278                            7   \n",
       "1  74dfe9a67327540d1f427b40e85d49c7                           28   \n",
       "2  078f92ffdfdfe07bb958c10c01d0733c                           33   \n",
       "3  c1d381f50813111b4f1f8d3b124c369f                           17   \n",
       "4  3f60ef34ad3853819e4269469280177d                           41   \n",
       "\n",
       "   same_page_consecutive_ratio  same_module_consecutive_count  \\\n",
       "0                     0.125000                             51   \n",
       "1                     0.210526                             42   \n",
       "2                     0.169231                             58   \n",
       "3                     0.122302                             59   \n",
       "4                     0.123867                             84   \n",
       "\n",
       "   same_module_consecutive_ratio  revisited_page_count  avg_page_visit_count  \\\n",
       "0                       0.910714                   4.0              9.333333   \n",
       "1                       0.315789                   8.0             12.090909   \n",
       "2                       0.297436                   9.0             10.833333   \n",
       "3                       0.424460                   8.0             10.692308   \n",
       "4                       0.253776                  20.0              8.710526   \n",
       "\n",
       "   visit_interval_mean  visit_interval_max  visit_interval_min  \\\n",
       "0             0.418182                 5.0                 0.0   \n",
       "1             0.681818                11.0                 0.0   \n",
       "2             0.453608                13.0                 0.0   \n",
       "3             0.623188                13.0                 0.0   \n",
       "4             0.269697                 8.0                 0.0   \n",
       "\n",
       "   visit_interval_std  total_sessions  session_visit_mean  session_visit_max  \\\n",
       "0            1.030838               6            8.000000                 21   \n",
       "1            1.982050              18            7.000000                 17   \n",
       "2            1.682095              16           11.470588                 40   \n",
       "3            1.993430              13            9.928571                 24   \n",
       "4            1.053287              21           15.045455                 49   \n",
       "\n",
       "   session_visit_std  recent_3_page_nunique  recent_3_module_nunique  \n",
       "0           5.916080                      3                        1  \n",
       "1           4.163332                      2                        2  \n",
       "2          10.648460                      3                        3  \n",
       "3           6.911131                      3                        3  \n",
       "4          12.518471                      3                        3  "
      ]
     },
     "execution_count": 20,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 9. 序列行为特征\n",
    "def gen_mb_sequence_features(df):\n",
    "    \"\"\"\n",
    "    基于访问序列的特征\n",
    "    关注用户访问的连续性、循环性等\n",
    "    \"\"\"\n",
    "    feature = df[[\"CUST_NO\"]].drop_duplicates(['CUST_NO']).copy().reset_index(drop=True)\n",
    "    \n",
    "    # 按时间排序\n",
    "    df_sorted = df.sort_values(['CUST_NO', 'OPERATION_DATE', 'date_days_to_now']).copy()\n",
    "    \n",
    "    # 9.1 页面访问序列特征\n",
    "    # 连续访问同一页面的次数\n",
    "    df_sorted['same_page'] = (df_sorted.groupby('CUST_NO')['PAGE_TITLE'].shift(1) == df_sorted['PAGE_TITLE']).astype(int)\n",
    "    tmp = df_sorted.groupby('CUST_NO')['same_page'].agg(['sum', 'mean'])\n",
    "    tmp.columns = ['same_page_consecutive_count', 'same_page_consecutive_ratio']\n",
    "    tmp = tmp.reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 9.2 模块访问序列特征\n",
    "    # 连续访问同一模块的次数\n",
    "    df_sorted['same_module'] = (df_sorted.groupby('CUST_NO')['MODEL_NAME'].shift(1) == df_sorted['MODEL_NAME']).astype(int)\n",
    "    tmp = df_sorted.groupby('CUST_NO')['same_module'].agg(['sum', 'mean'])\n",
    "    tmp.columns = ['same_module_consecutive_count', 'same_module_consecutive_ratio']\n",
    "    tmp = tmp.reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 9.3 页面回访特征\n",
    "    # 统计用户重复访问同一页面的情况\n",
    "    df_page_revisit = df.groupby(['CUST_NO', 'PAGE_TITLE']).size().reset_index(name='page_visit_count')\n",
    "    \n",
    "    # 有多少页面被访问超过1次\n",
    "    tmp = df_page_revisit[df_page_revisit['page_visit_count'] > 1].groupby('CUST_NO').size().to_frame(\n",
    "        'revisited_page_count'\n",
    "    ).reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 平均每个页面被访问次数\n",
    "    tmp = df_page_revisit.groupby('CUST_NO')['page_visit_count'].mean().to_frame(\n",
    "        'avg_page_visit_count'\n",
    "    ).reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 9.4 访问间隔特征\n",
    "    # 计算相邻两次访问的时间间隔\n",
    "    df_sorted['days_diff'] = df_sorted.groupby('CUST_NO')['date_days_to_now'].diff().abs()\n",
    "    \n",
    "    tmp = df_sorted.groupby('CUST_NO')['days_diff'].agg(['mean', 'max', 'min', 'std'])\n",
    "    tmp.columns = [f'visit_interval_{col}' for col in tmp.columns]\n",
    "    tmp = tmp.reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 9.5 会话特征 (假设间隔>1天为不同会话)\n",
    "    df_sorted['new_session'] = (df_sorted['days_diff'] > 1).astype(int)\n",
    "    df_sorted['session_id'] = df_sorted.groupby('CUST_NO')['new_session'].cumsum()\n",
    "    \n",
    "    # 会话数量\n",
    "    tmp = df_sorted.groupby('CUST_NO')['session_id'].max().to_frame('total_sessions').reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 平均每个会话的访问次数\n",
    "    session_counts = df_sorted.groupby(['CUST_NO', 'session_id']).size().reset_index(name='session_count')\n",
    "    tmp = session_counts.groupby('CUST_NO')['session_count'].agg(['mean', 'max', 'std'])\n",
    "    tmp.columns = [f'session_visit_{col}' for col in tmp.columns]\n",
    "    tmp = tmp.reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 9.6 最近N次访问的页面/模块\n",
    "    # 最近3次访问是否重复\n",
    "    df_recent = df_sorted.groupby('CUST_NO').tail(3)\n",
    "    tmp = df_recent.groupby('CUST_NO')['PAGE_TITLE'].nunique().to_frame('recent_3_page_nunique').reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    tmp = df_recent.groupby('CUST_NO')['MODEL_NAME'].nunique().to_frame('recent_3_module_nunique').reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    return feature\n",
    "\n",
    "mb_sequence_features = gen_mb_sequence_features(MB_PAGEVIEW_DTL_data)\n",
    "print(f\"✓ 序列行为特征数量: {mb_sequence_features.shape[1] - 1}\")\n",
    "mb_sequence_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5a74d365",
   "metadata": {},
   "source": [
    "## 第十部分: 合并所有特征并保存"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "e4d679a0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "================================================================================\n",
      "掌银页面访问明细表特征工程完成!\n",
      "================================================================================\n",
      "总特征数量: 502\n",
      "客户数量: 2753\n",
      "\n",
      "各部分特征数量:\n",
      "  1. 基础RFM特征: 18\n",
      "  2. 导航路径特征: 66\n",
      "  3. 页面/模块分组特征: 300\n",
      "  4. 时间段行为特征: 28\n",
      "  5. 用户习惯特征: 24\n",
      "  6. 趋势变化特征: 33\n",
      "  7. 交叉统计特征: 17\n",
      "  8. 序列行为特征: 16\n",
      "================================================================================\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>max_nunique_days_to_now_0</th>\n",
       "      <th>max_count_days_to_now_0</th>\n",
       "      <th>max_nunique_days_to_now_1</th>\n",
       "      <th>max_count_days_to_now_1</th>\n",
       "      <th>max_nunique_days_to_now_2</th>\n",
       "      <th>max_count_days_to_now_2</th>\n",
       "      <th>daily_page_nunique_mean</th>\n",
       "      <th>daily_page_count_mean</th>\n",
       "      <th>daily_page_nunique_max</th>\n",
       "      <th>...</th>\n",
       "      <th>visit_interval_mean</th>\n",
       "      <th>visit_interval_max</th>\n",
       "      <th>visit_interval_min</th>\n",
       "      <th>visit_interval_std</th>\n",
       "      <th>total_sessions</th>\n",
       "      <th>session_visit_mean</th>\n",
       "      <th>session_visit_max</th>\n",
       "      <th>session_visit_std</th>\n",
       "      <th>recent_3_page_nunique</th>\n",
       "      <th>recent_3_module_nunique</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0004b47a5be96a8379e58932489ca6da</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>61.0</td>\n",
       "      <td>61.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>18.0</td>\n",
       "      <td>37.0</td>\n",
       "      <td>18</td>\n",
       "      <td>...</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0</td>\n",
       "      <td>37.0</td>\n",
       "      <td>37</td>\n",
       "      <td>NaN</td>\n",
       "      <td>3</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>00347b0bb271750962f327d2d5bbf737</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>35.0</td>\n",
       "      <td>35.0</td>\n",
       "      <td>69.0</td>\n",
       "      <td>69.0</td>\n",
       "      <td>11.0</td>\n",
       "      <td>15.0</td>\n",
       "      <td>15</td>\n",
       "      <td>...</td>\n",
       "      <td>0.795455</td>\n",
       "      <td>34.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>5.124404</td>\n",
       "      <td>1</td>\n",
       "      <td>22.5</td>\n",
       "      <td>29</td>\n",
       "      <td>9.192388</td>\n",
       "      <td>3</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>00457ecc79efe086078ac66df918d059</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>79.0</td>\n",
       "      <td>79.0</td>\n",
       "      <td>5.0</td>\n",
       "      <td>5.0</td>\n",
       "      <td>5</td>\n",
       "      <td>...</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0</td>\n",
       "      <td>5.0</td>\n",
       "      <td>5</td>\n",
       "      <td>NaN</td>\n",
       "      <td>3</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>006a69967d1cddb741798cd43e4a6ddc</td>\n",
       "      <td>6.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>38.0</td>\n",
       "      <td>46.0</td>\n",
       "      <td>90.0</td>\n",
       "      <td>90.0</td>\n",
       "      <td>8.5</td>\n",
       "      <td>12.5</td>\n",
       "      <td>10</td>\n",
       "      <td>...</td>\n",
       "      <td>1.714286</td>\n",
       "      <td>44.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>7.745967</td>\n",
       "      <td>3</td>\n",
       "      <td>12.5</td>\n",
       "      <td>15</td>\n",
       "      <td>2.380476</td>\n",
       "      <td>3</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>006d0ffe16adb65bc9afddb229eb6e88</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>87.0</td>\n",
       "      <td>87.0</td>\n",
       "      <td>2.5</td>\n",
       "      <td>3.0</td>\n",
       "      <td>3</td>\n",
       "      <td>...</td>\n",
       "      <td>1.000000</td>\n",
       "      <td>5.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>2.236068</td>\n",
       "      <td>1</td>\n",
       "      <td>3.0</td>\n",
       "      <td>4</td>\n",
       "      <td>1.414214</td>\n",
       "      <td>3</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>5 rows × 503 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  max_nunique_days_to_now_0  \\\n",
       "0  0004b47a5be96a8379e58932489ca6da                        NaN   \n",
       "1  00347b0bb271750962f327d2d5bbf737                        NaN   \n",
       "2  00457ecc79efe086078ac66df918d059                        NaN   \n",
       "3  006a69967d1cddb741798cd43e4a6ddc                        6.0   \n",
       "4  006d0ffe16adb65bc9afddb229eb6e88                        NaN   \n",
       "\n",
       "   max_count_days_to_now_0  max_nunique_days_to_now_1  \\\n",
       "0                      NaN                       61.0   \n",
       "1                      NaN                       35.0   \n",
       "2                      NaN                        NaN   \n",
       "3                      6.0                       38.0   \n",
       "4                      NaN                        NaN   \n",
       "\n",
       "   max_count_days_to_now_1  max_nunique_days_to_now_2  \\\n",
       "0                     61.0                        NaN   \n",
       "1                     35.0                       69.0   \n",
       "2                      NaN                       79.0   \n",
       "3                     46.0                       90.0   \n",
       "4                      NaN                       87.0   \n",
       "\n",
       "   max_count_days_to_now_2  daily_page_nunique_mean  daily_page_count_mean  \\\n",
       "0                      NaN                     18.0                   37.0   \n",
       "1                     69.0                     11.0                   15.0   \n",
       "2                     79.0                      5.0                    5.0   \n",
       "3                     90.0                      8.5                   12.5   \n",
       "4                     87.0                      2.5                    3.0   \n",
       "\n",
       "   daily_page_nunique_max  ...  visit_interval_mean  visit_interval_max  \\\n",
       "0                      18  ...             0.000000                 0.0   \n",
       "1                      15  ...             0.795455                34.0   \n",
       "2                       5  ...             0.000000                 0.0   \n",
       "3                      10  ...             1.714286                44.0   \n",
       "4                       3  ...             1.000000                 5.0   \n",
       "\n",
       "   visit_interval_min  visit_interval_std  total_sessions  session_visit_mean  \\\n",
       "0                 0.0            0.000000               0                37.0   \n",
       "1                 0.0            5.124404               1                22.5   \n",
       "2                 0.0            0.000000               0                 5.0   \n",
       "3                 0.0            7.745967               3                12.5   \n",
       "4                 0.0            2.236068               1                 3.0   \n",
       "\n",
       "   session_visit_max  session_visit_std  recent_3_page_nunique  \\\n",
       "0                 37                NaN                      3   \n",
       "1                 29           9.192388                      3   \n",
       "2                  5                NaN                      3   \n",
       "3                 15           2.380476                      3   \n",
       "4                  4           1.414214                      3   \n",
       "\n",
       "   recent_3_module_nunique  \n",
       "0                        2  \n",
       "1                        2  \n",
       "2                        2  \n",
       "3                        3  \n",
       "4                        2  \n",
       "\n",
       "[5 rows x 503 columns]"
      ]
     },
     "execution_count": 21,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 10. 合并所有特征\n",
    "from functools import reduce\n",
    "\n",
    "# 合并所有特征集\n",
    "feature_dfs = [\n",
    "    mb_basic_features,\n",
    "    mb_navigation_features,\n",
    "    mb_page_module_features,\n",
    "    mb_time_period_features,\n",
    "    mb_user_habit_features,\n",
    "    mb_trend_features,\n",
    "    mb_cross_features,\n",
    "    mb_sequence_features\n",
    "]\n",
    "\n",
    "# 使用reduce进行多个DataFrame的合并\n",
    "MB_PAGEVIEW_DTL_features = reduce(lambda left, right: pd.merge(left, right, on='CUST_NO', how='outer'), feature_dfs)\n",
    "\n",
    "print(\"=\"*80)\n",
    "print(\"掌银页面访问明细表特征工程完成!\")\n",
    "print(\"=\"*80)\n",
    "print(f\"总特征数量: {MB_PAGEVIEW_DTL_features.shape[1] - 1}\")  # 减去CUST_NO列\n",
    "print(f\"客户数量: {MB_PAGEVIEW_DTL_features.shape[0]}\")\n",
    "print(f\"\\n各部分特征数量:\")\n",
    "print(f\"  1. 基础RFM特征: {mb_basic_features.shape[1] - 1}\")\n",
    "print(f\"  2. 导航路径特征: {mb_navigation_features.shape[1] - 1}\")\n",
    "print(f\"  3. 页面/模块分组特征: {mb_page_module_features.shape[1] - 1}\")\n",
    "print(f\"  4. 时间段行为特征: {mb_time_period_features.shape[1] - 1}\")\n",
    "print(f\"  5. 用户习惯特征: {mb_user_habit_features.shape[1] - 1}\")\n",
    "print(f\"  6. 趋势变化特征: {mb_trend_features.shape[1] - 1}\")\n",
    "print(f\"  7. 交叉统计特征: {mb_cross_features.shape[1] - 1}\")\n",
    "print(f\"  8. 序列行为特征: {mb_sequence_features.shape[1] - 1}\")\n",
    "print(\"=\"*80)\n",
    "\n",
    "# 显示前几行\n",
    "MB_PAGEVIEW_DTL_features.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "1a574515",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✅ 特征已保存到: ./feature/mb_pageview_dtl_features.pkl\n",
      "   文件大小: 10.60 MB\n",
      "✓ 特征名称列表已保存到: feature\\MB_PAGEVIEW_DTL_feature_names.txt\n",
      "\n",
      "缺失值统计(前20个):\n",
      "module_1993162004f0f20fe0bcda9337e150b4_last_visit                                    2747\n",
      "module_1993162004f0f20fe0bcda9337e150b4_unique_pages                                  2747\n",
      "module_1993162004f0f20fe0bcda9337e150b4_count                                         2747\n",
      "module_2e277a07f4ce93a6b31e927c7db8832f_count                                         2744\n",
      "module_27145c21dd5cc647184d3b477607dfd4_count                                         2744\n",
      "module_27145c21dd5cc647184d3b477607dfd4_unique_pages                                  2744\n",
      "module_27145c21dd5cc647184d3b477607dfd4_last_visit                                    2744\n",
      "module_2e277a07f4ce93a6b31e927c7db8832f_unique_pages                                  2744\n",
      "module_2e277a07f4ce93a6b31e927c7db8832f_last_visit                                    2744\n",
      "module_a6ae7497f8921a2ce0058e4174116ff5_last_visit                                    2731\n",
      "module_a6ae7497f8921a2ce0058e4174116ff5_unique_pages                                  2731\n",
      "module_a6ae7497f8921a2ce0058e4174116ff5_count                                         2731\n",
      "module_d166e7d89059242e8210b0b647206cb1_count                                         2725\n",
      "module_d166e7d89059242e8210b0b647206cb1_last_visit                                    2725\n",
      "module_d166e7d89059242e8210b0b647206cb1_unique_pages                                  2725\n",
      "model_path_count_c5b386b7a6348a2f1ba70f2259fb827e_56e6187b5b44989eea9322ccb9443036    2721\n",
      "module_2613588ce2fcc962c5511a4e3eb234ba_count                                         2718\n",
      "module_2613588ce2fcc962c5511a4e3eb234ba_unique_pages                                  2718\n",
      "module_2613588ce2fcc962c5511a4e3eb234ba_last_visit                                    2718\n",
      "module_56e6187b5b44989eea9322ccb9443036_count                                         2715\n",
      "dtype: int64\n"
     ]
    }
   ],
   "source": [
    "# 11. 保存特征\n",
    "import os\n",
    "\n",
    "# 确保feature目录存在\n",
    "feature_dir = 'feature'\n",
    "if not os.path.exists(feature_dir):\n",
    "    os.makedirs(feature_dir)\n",
    "\n",
    "# 保存为pickle格式\n",
    "feature_path = './feature/mb_pageview_dtl_features.pkl'\n",
    "MB_PAGEVIEW_DTL_features.to_pickle(feature_path)\n",
    "print(f\"✅ 特征已保存到: {feature_path}\")\n",
    "print(f\"   文件大小: {os.path.getsize(feature_path) / 1024 / 1024:.2f} MB\")\n",
    "\n",
    "# 保存特征名称列表\n",
    "feature_names = [col for col in MB_PAGEVIEW_DTL_features.columns if col != 'CUST_NO']\n",
    "feature_names_file = os.path.join(feature_dir, 'MB_PAGEVIEW_DTL_feature_names.txt')\n",
    "with open(feature_names_file, 'w', encoding='utf-8') as f:\n",
    "    for name in feature_names:\n",
    "        f.write(name + '\\n')\n",
    "print(f\"✓ 特征名称列表已保存到: {feature_names_file}\")\n",
    "\n",
    "# 检查缺失值情况\n",
    "missing_info = MB_PAGEVIEW_DTL_features.isnull().sum()\n",
    "missing_info = missing_info[missing_info > 0].sort_values(ascending=False)\n",
    "print(f\"\\n缺失值统计(前20个):\")\n",
    "print(missing_info.head(20))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8a45680f",
   "metadata": {},
   "source": [
    "## 特征工程总结\n",
    "\n",
    "### 特征设计思路分析\n",
    "\n",
    "基于对参考文件 `1.lgb_A榜.ipynb` 的深入分析,我们设计了以下9大类特征:\n",
    "\n",
    "#### 1. **基础RFM特征** (Recency, Frequency, Monetary)\n",
    "- **Recency(最近性)**: 每月日点击次数/页面数最大天距今天数\n",
    "- **Frequency(频率)**: 每日点击统计量(均值、最大值、标准差等)\n",
    "- 参考代码中的核心逻辑: 通过`get_max_cnt_days_to_now`函数捕捉用户最活跃的时间点\n",
    "\n",
    "#### 2. **导航路径特征** (Navigation Path)\n",
    "- 页面跳转路径: `REFERRER_TITLE` → `PAGE_TITLE`\n",
    "- 模块跳转路径: `REFERRER_MODEL_NAME` → `MODEL_NAME`\n",
    "- 路径多样性、Top路径访问频次\n",
    "- 参考文件中使用了页面/模块拼接方式创建路径特征\n",
    "\n",
    "#### 3. **页面/模块分组特征**\n",
    "- Top60页面的访问次数、访问天数、最近访问时间\n",
    "- Top40模块的访问次数、不同页面数、最近访问时间\n",
    "- 参考文件中对高频页面和模块进行了重点统计\n",
    "\n",
    "#### 4. **时间段行为特征**\n",
    "- 按月份(0/1/2)统计访问行为\n",
    "- 工作日vs周末访问差异\n",
    "- 月初/月末行为、按周几统计\n",
    "- 参考文件中使用`date_months_to_now`、`date_weeks_to_now`进行时间分桶\n",
    "\n",
    "#### 5. **用户习惯特征**\n",
    "- 访问集中度、平均每天访问页面数\n",
    "- 页面浏览深度、模块切换频率\n",
    "- 访问规律性、首末次访问特征\n",
    "\n",
    "#### 6. **趋势变化特征**\n",
    "- 月度变化率(环比、同比)\n",
    "- 访问加速度(二阶差分)\n",
    "- 访问趋势判断(上升/下降)\n",
    "- 周度变化系数\n",
    "\n",
    "#### 7. **交叉统计特征**\n",
    "- 模块-页面交叉\n",
    "- 时间-页面/模块交叉\n",
    "- 周几-访问量交叉\n",
    "- 各种比例类特征\n",
    "\n",
    "#### 8. **序列行为特征**\n",
    "- 连续访问同一页面/模块次数\n",
    "- 页面回访特征\n",
    "- 访问间隔统计\n",
    "- 会话(Session)特征\n",
    "- 最近N次访问特征\n",
    "\n",
    "### 关键技术点\n",
    "\n",
    "1. **时间特征转换**: 将日期转换为距今天数、周数、月数\n",
    "2. **分组统计**: 使用pandas的groupby + agg进行多维度统计\n",
    "3. **Pivot透视**: 将类别特征转换为多列数值特征\n",
    "4. **路径拼接**: 通过字符串拼接创建访问路径特征\n",
    "5. **趋势捕捉**: 通过环比、同比、加速度等方式捕捉变化趋势\n",
    "\n",
    "### 特征数量控制\n",
    "\n",
    "- 总特征数控制在800以内\n",
    "- 通过选择Top页面/模块(Top60页面+Top40模块)来控制特征数量\n",
    "- 避免过多的稀疏特征\n",
    "\n",
    "### 后续优化方向\n",
    "\n",
    "1. 可以考虑Word2Vec对页面序列进行嵌入\n",
    "2. 可以添加更多的统计量(分位数、偏度、峰度等)\n",
    "3. 可以尝试更复杂的路径模式挖掘\n",
    "4. 可以结合其他表的特征进行联合分析"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "starcup",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.18"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
